codekingpro/portable-devtools
115k
1from __future__ import annotations2 3import os.path4import re5import sys6import textwrap7from abc import ABC, abstractmethod8from pathlib import Path9from typing import (10 TYPE_CHECKING,11 Any,12 Dict,13 Iterable,14 List,15 NamedTuple,16 Optional,17 Sequence,18 Set,19 Tuple,20 Type,21 Union,22)23 24from pygments.lexer import Lexer25from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename26from pygments.style import Style as PygmentsStyle27from pygments.styles import get_style_by_name28from pygments.token import (29 Comment,30 Error,31 Generic,32 Keyword,33 Name,34 Number,35 Operator,36 String,37 Token,38 Whitespace,39)40from pygments.util import ClassNotFound41 42if TYPE_CHECKING:43 from .console import Console, ConsoleOptions, JustifyMethod, RenderResult44 45from rich.containers import Lines46from rich.padding import Padding, PaddingDimensions47 48from ._loop import loop_first49from .cells import cell_len50from .color import Color, blend_rgb51from .jupyter import JupyterMixin52from .measure import Measurement53from .segment import Segment, Segments54from .style import Style, StyleType55from .text import Text56 57TokenType = Tuple[str, ...]58 59WINDOWS = sys.platform == "win32"60DEFAULT_THEME = "monokai"61 62# The following styles are based on https://github.com/pygments/pygments/blob/master/pygments/formatters/terminal.py63# A few modifications were made64 65ANSI_LIGHT: Dict[TokenType, Style] = {66 Token: Style(),67 Whitespace: Style(color="white"),68 Comment: Style(dim=True),69 Comment.Preproc: Style(color="cyan"),70 Keyword: Style(color="blue"),71 Keyword.Type: Style(color="cyan"),72 Operator.Word: Style(color="magenta"),73 Name.Builtin: Style(color="cyan"),74 Name.Function: Style(color="green"),75 Name.Namespace: Style(color="cyan", underline=True),76 Name.Class: Style(color="green", underline=True),77 Name.Exception: Style(color="cyan"),78 Name.Decorator: Style(color="magenta", bold=True),79 Name.Variable: Style(color="red"),80 Name.Constant: Style(color="red"),81 Name.Attribute: Style(color="cyan"),82 Name.Tag: Style(color="bright_blue"),83 String: Style(color="yellow"),84 Number: Style(color="blue"),85 Generic.Deleted: Style(color="bright_red"),86 Generic.Inserted: Style(color="green"),87 Generic.Heading: Style(bold=True),88 Generic.Subheading: Style(color="magenta", bold=True),89 Generic.Prompt: Style(bold=True),90 Generic.Error: Style(color="bright_red"),91 Error: Style(color="red", underline=True),92}93 94ANSI_DARK: Dict[TokenType, Style] = {95 Token: Style(),96 Whitespace: Style(color="bright_black"),97 Comment: Style(dim=True),98 Comment.Preproc: Style(color="bright_cyan"),99 Keyword: Style(color="bright_blue"),100 Keyword.Type: Style(color="bright_cyan"),101 Operator.Word: Style(color="bright_magenta"),102 Name.Builtin: Style(color="bright_cyan"),103 Name.Function: Style(color="bright_green"),104 Name.Namespace: Style(color="bright_cyan", underline=True),105 Name.Class: Style(color="bright_green", underline=True),106 Name.Exception: Style(color="bright_cyan"),107 Name.Decorator: Style(color="bright_magenta", bold=True),108 Name.Variable: Style(color="bright_red"),109 Name.Constant: Style(color="bright_red"),110 Name.Attribute: Style(color="bright_cyan"),111 Name.Tag: Style(color="bright_blue"),112 String: Style(color="yellow"),113 Number: Style(color="bright_blue"),114 Generic.Deleted: Style(color="bright_red"),115 Generic.Inserted: Style(color="bright_green"),116 Generic.Heading: Style(bold=True),117 Generic.Subheading: Style(color="bright_magenta", bold=True),118 Generic.Prompt: Style(bold=True),119 Generic.Error: Style(color="bright_red"),120 Error: Style(color="red", underline=True),121}122 123RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK}124NUMBERS_COLUMN_DEFAULT_PADDING = 2125 126 127class SyntaxTheme(ABC):128 """Base class for a syntax theme."""129 130 @abstractmethod131 def get_style_for_token(self, token_type: TokenType) -> Style:132 """Get a style for a given Pygments token."""133 raise NotImplementedError # pragma: no cover134 135 @abstractmethod136 def get_background_style(self) -> Style:137 """Get the background color."""138 raise NotImplementedError # pragma: no cover139 140 141class PygmentsSyntaxTheme(SyntaxTheme):142 """Syntax theme that delegates to Pygments theme."""143 144 def __init__(self, theme: Union[str, Type[PygmentsStyle]]) -> None:145 self._style_cache: Dict[TokenType, Style] = {}146 if isinstance(theme, str):147 try:148 self._pygments_style_class = get_style_by_name(theme)149 except ClassNotFound:150 self._pygments_style_class = get_style_by_name("default")151 else:152 self._pygments_style_class = theme153 154 self._background_color = self._pygments_style_class.background_color155 self._background_style = Style(bgcolor=self._background_color)156 157 def get_style_for_token(self, token_type: TokenType) -> Style:158 """Get a style from a Pygments class."""159 try:160 return self._style_cache[token_type]161 except KeyError:162 try:163 pygments_style = self._pygments_style_class.style_for_token(token_type)164 except KeyError:165 style = Style.null()166 else:167 color = pygments_style["color"]168 bgcolor = pygments_style["bgcolor"]169 style = Style(170 color="#" + color if color else "#000000",171 bgcolor="#" + bgcolor if bgcolor else self._background_color,172 bold=pygments_style["bold"],173 italic=pygments_style["italic"],174 underline=pygments_style["underline"],175 )176 self._style_cache[token_type] = style177 return style178 179 def get_background_style(self) -> Style:180 return self._background_style181 182 183class ANSISyntaxTheme(SyntaxTheme):184 """Syntax theme to use standard colors."""185 186 def __init__(self, style_map: Dict[TokenType, Style]) -> None:187 self.style_map = style_map188 self._missing_style = Style.null()189 self._background_style = Style.null()190 self._style_cache: Dict[TokenType, Style] = {}191 192 def get_style_for_token(self, token_type: TokenType) -> Style:193 """Look up style in the style map."""194 try:195 return self._style_cache[token_type]196 except KeyError:197 # Styles form a hierarchy198 # We need to go from most to least specific199 # e.g. ("foo", "bar", "baz") to ("foo", "bar") to ("foo",)200 get_style = self.style_map.get201 token = tuple(token_type)202 style = self._missing_style203 while token:204 _style = get_style(token)205 if _style is not None:206 style = _style207 break208 token = token[:-1]209 self._style_cache[token_type] = style210 return style211 212 def get_background_style(self) -> Style:213 return self._background_style214 215 216SyntaxPosition = Tuple[int, int]217 218 219class _SyntaxHighlightRange(NamedTuple):220 """221 A range to highlight in a Syntax object.222 `start` and `end` are 2-integers tuples, where the first integer is the line number223 (starting from 1) and the second integer is the column index (starting from 0).224 """225 226 style: StyleType227 start: SyntaxPosition228 end: SyntaxPosition229 style_before: bool = False230 231 232class PaddingProperty:233 """Descriptor to get and set padding."""234 235 def __get__(self, obj: Syntax, objtype: Type[Syntax]) -> Tuple[int, int, int, int]:236 """Space around the Syntax."""237 return obj._padding238 239 def __set__(self, obj: Syntax, padding: PaddingDimensions) -> None:240 obj._padding = Padding.unpack(padding)241 242 243class Syntax(JupyterMixin):244 """Construct a Syntax object to render syntax highlighted code.245 246 Args:247 code (str): Code to highlight.248 lexer (Lexer | str): Lexer to use (see https://pygments.org/docs/lexers/)249 theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "monokai".250 dedent (bool, optional): Enable stripping of initial whitespace. Defaults to False.251 line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.252 start_line (int, optional): Starting number for line numbers. Defaults to 1.253 line_range (Tuple[int | None, int | None], optional): If given should be a tuple of the start and end line to render.254 A value of None in the tuple indicates the range is open in that direction.255 highlight_lines (Set[int]): A set of line numbers to highlight.256 code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.257 tab_size (int, optional): Size of tabs. Defaults to 4.258 word_wrap (bool, optional): Enable word wrapping.259 background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.260 indent_guides (bool, optional): Show indent guides. Defaults to False.261 padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).262 """263 264 _pygments_style_class: Type[PygmentsStyle]265 _theme: SyntaxTheme266 267 @classmethod268 def get_theme(cls, name: Union[str, SyntaxTheme]) -> SyntaxTheme:269 """Get a syntax theme instance."""270 if isinstance(name, SyntaxTheme):271 return name272 theme: SyntaxTheme273 if name in RICH_SYNTAX_THEMES:274 theme = ANSISyntaxTheme(RICH_SYNTAX_THEMES[name])275 else:276 theme = PygmentsSyntaxTheme(name)277 return theme278 279 def __init__(280 self,281 code: str,282 lexer: Union[Lexer, str],283 *,284 theme: Union[str, SyntaxTheme] = DEFAULT_THEME,285 dedent: bool = False,286 line_numbers: bool = False,287 start_line: int = 1,288 line_range: Optional[Tuple[Optional[int], Optional[int]]] = None,289 highlight_lines: Optional[Set[int]] = None,290 code_width: Optional[int] = None,291 tab_size: int = 4,292 word_wrap: bool = False,293 background_color: Optional[str] = None,294 indent_guides: bool = False,295 padding: PaddingDimensions = 0,296 ) -> None:297 self.code = code298 self._lexer = lexer299 self.dedent = dedent300 self.line_numbers = line_numbers301 self.start_line = start_line302 self.line_range = line_range303 self.highlight_lines = highlight_lines or set()304 self.code_width = code_width305 self.tab_size = tab_size306 self.word_wrap = word_wrap307 self.background_color = background_color308 self.background_style = (309 Style(bgcolor=background_color) if background_color else Style()310 )311 self.indent_guides = indent_guides312 self._padding = Padding.unpack(padding)313 314 self._theme = self.get_theme(theme)315 self._stylized_ranges: List[_SyntaxHighlightRange] = []316 317 padding = PaddingProperty()318 319 @classmethod320 def from_path(321 cls,322 path: str,323 encoding: str = "utf-8",324 lexer: Optional[Union[Lexer, str]] = None,325 theme: Union[str, SyntaxTheme] = DEFAULT_THEME,326 dedent: bool = False,327 line_numbers: bool = False,328 line_range: Optional[Tuple[int, int]] = None,329 start_line: int = 1,330 highlight_lines: Optional[Set[int]] = None,331 code_width: Optional[int] = None,332 tab_size: int = 4,333 word_wrap: bool = False,334 background_color: Optional[str] = None,335 indent_guides: bool = False,336 padding: PaddingDimensions = 0,337 ) -> "Syntax":338 """Construct a Syntax object from a file.339 340 Args:341 path (str): Path to file to highlight.342 encoding (str): Encoding of file.343 lexer (str | Lexer, optional): Lexer to use. If None, lexer will be auto-detected from path/file content.344 theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "emacs".345 dedent (bool, optional): Enable stripping of initial whitespace. Defaults to True.346 line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.347 start_line (int, optional): Starting number for line numbers. Defaults to 1.348 line_range (Tuple[int, int], optional): If given should be a tuple of the start and end line to render.349 highlight_lines (Set[int]): A set of line numbers to highlight.350 code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.351 tab_size (int, optional): Size of tabs. Defaults to 4.352 word_wrap (bool, optional): Enable word wrapping of code.353 background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.354 indent_guides (bool, optional): Show indent guides. Defaults to False.355 padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).356 357 Returns:358 [Syntax]: A Syntax object that may be printed to the console359 """360 code = Path(path).read_text(encoding=encoding)361 362 if not lexer:363 lexer = cls.guess_lexer(path, code=code)364 365 return cls(366 code,367 lexer,368 theme=theme,369 dedent=dedent,370 line_numbers=line_numbers,371 line_range=line_range,372 start_line=start_line,373 highlight_lines=highlight_lines,374 code_width=code_width,375 tab_size=tab_size,376 word_wrap=word_wrap,377 background_color=background_color,378 indent_guides=indent_guides,379 padding=padding,380 )381 382 @classmethod383 def guess_lexer(cls, path: str, code: Optional[str] = None) -> str:384 """Guess the alias of the Pygments lexer to use based on a path and an optional string of code.385 If code is supplied, it will use a combination of the code and the filename to determine the386 best lexer to use. For example, if the file is ``index.html`` and the file contains Django387 templating syntax, then "html+django" will be returned. If the file is ``index.html``, and no388 templating language is used, the "html" lexer will be used. If no string of code389 is supplied, the lexer will be chosen based on the file extension..390 391 Args:392 path (AnyStr): The path to the file containing the code you wish to know the lexer for.393 code (str, optional): Optional string of code that will be used as a fallback if no lexer394 is found for the supplied path.395 396 Returns:397 str: The name of the Pygments lexer that best matches the supplied path/code.398 """399 lexer: Optional[Lexer] = None400 lexer_name = "default"401 if code:402 try:403 lexer = guess_lexer_for_filename(path, code)404 except ClassNotFound:405 pass406 407 if not lexer:408 try:409 _, ext = os.path.splitext(path)410 if ext:411 extension = ext.lstrip(".").lower()412 lexer = get_lexer_by_name(extension)413 except ClassNotFound:414 pass415 416 if lexer:417 if lexer.aliases:418 lexer_name = lexer.aliases[0]419 else:420 lexer_name = lexer.name421 422 return lexer_name423 424 def _get_base_style(self) -> Style:425 """Get the base style."""426 default_style = self._theme.get_background_style() + self.background_style427 return default_style428 429 def _get_token_color(self, token_type: TokenType) -> Optional[Color]:430 """Get a color (if any) for the given token.431 432 Args:433 token_type (TokenType): A token type tuple from Pygments.434 435 Returns:436 Optional[Color]: Color from theme, or None for no color.437 """438 style = self._theme.get_style_for_token(token_type)439 return style.color440 441 @property442 def lexer(self) -> Optional[Lexer]:443 """The lexer for this syntax, or None if no lexer was found.444 445 Tries to find the lexer by name if a string was passed to the constructor.446 """447 448 if isinstance(self._lexer, Lexer):449 return self._lexer450 try:451 return get_lexer_by_name(452 self._lexer,453 stripnl=False,454 ensurenl=True,455 tabsize=self.tab_size,456 )457 except ClassNotFound:458 return None459 460 @property461 def default_lexer(self) -> Lexer:462 """A Pygments Lexer to use if one is not specified or invalid."""463 return get_lexer_by_name(464 "text",465 stripnl=False,466 ensurenl=True,467 tabsize=self.tab_size,468 )469 470 def highlight(471 self,472 code: str,473 line_range: Optional[Tuple[Optional[int], Optional[int]]] = None,474 ) -> Text:475 """Highlight code and return a Text instance.476 477 Args:478 code (str): Code to highlight.479 line_range(Tuple[int, int], optional): Optional line range to highlight.480 481 Returns:482 Text: A text instance containing highlighted syntax.483 """484 485 base_style = self._get_base_style()486 justify: JustifyMethod = (487 "default" if base_style.transparent_background else "left"488 )489 490 text = Text(491 justify=justify,492 style=base_style,493 tab_size=self.tab_size,494 no_wrap=not self.word_wrap,495 )496 _get_theme_style = self._theme.get_style_for_token497 498 lexer = self.lexer or self.default_lexer499 500 if lexer is None:501 text.append(code)502 else:503 if line_range:504 # More complicated path to only stylize a portion of the code505 # This speeds up further operations as there are less spans to process506 line_start, line_end = line_range507 508 def line_tokenize() -> Iterable[Tuple[Any, str]]:509 """Split tokens to one per line."""510 assert lexer # required to make MyPy happy - we know lexer is not None at this point511 512 for token_type, token in lexer.get_tokens(code):513 while token:514 line_token, new_line, token = token.partition("\n")515 yield token_type, line_token + new_line516 517 def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]:518 """Convert tokens to spans."""519 tokens = iter(line_tokenize())520 line_no = 0521 _line_start = line_start - 1 if line_start else 0522 523 # Skip over tokens until line start524 while line_no < _line_start:525 try:526 _token_type, token = next(tokens)527 except StopIteration:528 break529 yield (token, None)530 if token.endswith("\n"):531 line_no += 1532 # Generate spans until line end533 for token_type, token in tokens:534 yield (token, _get_theme_style(token_type))535 if token.endswith("\n"):536 line_no += 1537 if line_end and line_no >= line_end:538 break539 540 text.append_tokens(tokens_to_spans())541 542 else:543 text.append_tokens(544 (token, _get_theme_style(token_type))545 for token_type, token in lexer.get_tokens(code)546 )547 if self.background_color is not None:548 text.stylize(f"on {self.background_color}")549 550 if self._stylized_ranges:551 self._apply_stylized_ranges(text)552 553 return text554 555 def stylize_range(556 self,557 style: StyleType,558 start: SyntaxPosition,559 end: SyntaxPosition,560 style_before: bool = False,561 ) -> None:562 """563 Adds a custom style on a part of the code, that will be applied to the syntax display when it's rendered.564 Line numbers are 1-based, while column indexes are 0-based.565 566 Args:567 style (StyleType): The style to apply.568 start (Tuple[int, int]): The start of the range, in the form `[line number, column index]`.569 end (Tuple[int, int]): The end of the range, in the form `[line number, column index]`.570 style_before (bool): Apply the style before any existing styles.571 """572 self._stylized_ranges.append(573 _SyntaxHighlightRange(style, start, end, style_before)574 )575 576 def _get_line_numbers_color(self, blend: float = 0.3) -> Color:577 background_style = self._theme.get_background_style() + self.background_style578 background_color = background_style.bgcolor579 if background_color is None or background_color.is_system_defined:580 return Color.default()581 foreground_color = self._get_token_color(Token.Text)582 if foreground_color is None or foreground_color.is_system_defined:583 return foreground_color or Color.default()584 new_color = blend_rgb(585 background_color.get_truecolor(),586 foreground_color.get_truecolor(),587 cross_fade=blend,588 )589 return Color.from_triplet(new_color)590 591 @property592 def _numbers_column_width(self) -> int:593 """Get the number of characters used to render the numbers column."""594 column_width = 0595 if self.line_numbers:596 column_width = (597 len(str(self.start_line + self.code.count("\n")))598 + NUMBERS_COLUMN_DEFAULT_PADDING599 )600 return column_width601 602 def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]:603 """Get background, number, and highlight styles for line numbers."""604 background_style = self._get_base_style()605 if background_style.transparent_background:606 return Style.null(), Style(dim=True), Style.null()607 if console.color_system in ("256", "truecolor"):608 number_style = Style.chain(609 background_style,610 self._theme.get_style_for_token(Token.Text),611 Style(color=self._get_line_numbers_color()),612 self.background_style,613 )614 highlight_number_style = Style.chain(615 background_style,616 self._theme.get_style_for_token(Token.Text),617 Style(bold=True, color=self._get_line_numbers_color(0.9)),618 self.background_style,619 )620 else:621 number_style = background_style + Style(dim=True)622 highlight_number_style = background_style + Style(dim=False)623 return background_style, number_style, highlight_number_style624 625 def __rich_measure__(626 self, console: "Console", options: "ConsoleOptions"627 ) -> "Measurement":628 _, right, _, left = self.padding629 padding = left + right630 if self.code_width is not None:631 width = self.code_width + self._numbers_column_width + padding + 1632 return Measurement(self._numbers_column_width, width)633 lines = self.code.splitlines()634 width = (635 self._numbers_column_width636 + padding637 + (max(cell_len(line) for line in lines) if lines else 0)638 )639 if self.line_numbers:640 width += 1641 return Measurement(self._numbers_column_width, width)642 643 def __rich_console__(644 self, console: Console, options: ConsoleOptions645 ) -> RenderResult:646 segments = Segments(self._get_syntax(console, options))647 if any(self.padding):648 yield Padding(segments, style=self._get_base_style(), pad=self.padding)649 else:650 yield segments651 652 def _get_syntax(653 self,654 console: Console,655 options: ConsoleOptions,656 ) -> Iterable[Segment]:657 """658 Get the Segments for the Syntax object, excluding any vertical/horizontal padding659 """660 transparent_background = self._get_base_style().transparent_background661 _pad_top, pad_right, _pad_bottom, pad_left = self.padding662 horizontal_padding = pad_left + pad_right663 code_width = (664 (665 (options.max_width - self._numbers_column_width - 1)666 if self.line_numbers667 else options.max_width668 )669 - horizontal_padding670 if self.code_width is None671 else self.code_width672 )673 code_width = max(0, code_width)674 675 ends_on_nl, processed_code = self._process_code(self.code)676 text = self.highlight(processed_code, self.line_range)677 678 if not self.line_numbers and not self.word_wrap and not self.line_range:679 if not ends_on_nl:680 text.remove_suffix("\n")681 # Simple case of just rendering text682 style = (683 self._get_base_style()684 + self._theme.get_style_for_token(Comment)685 + Style(dim=True)686 + self.background_style687 )688 if self.indent_guides and not options.ascii_only:689 text = text.with_indent_guides(self.tab_size, style=style)690 text.overflow = "crop"691 if style.transparent_background:692 yield from console.render(693 text, options=options.update(width=code_width)694 )695 else:696 syntax_lines = console.render_lines(697 text,698 options.update(width=code_width, height=None, justify="left"),699 style=self.background_style,700 pad=True,701 new_lines=True,702 )703 for syntax_line in syntax_lines:704 yield from syntax_line705 return706 707 start_line, end_line = self.line_range or (None, None)708 line_offset = 0709 if start_line:710 line_offset = max(0, start_line - 1)711 lines: Union[List[Text], Lines] = text.split("\n", allow_blank=ends_on_nl)712 if self.line_range:713 if line_offset > len(lines):714 return715 lines = lines[line_offset:end_line]716 717 if self.indent_guides and not options.ascii_only:718 style = (719 self._get_base_style()720 + self._theme.get_style_for_token(Comment)721 + Style(dim=True)722 + self.background_style723 )724 lines = (725 Text("\n")726 .join(lines)727 .with_indent_guides(self.tab_size, style=style + Style(italic=False))728 .split("\n", allow_blank=True)729 )730 731 numbers_column_width = self._numbers_column_width732 render_options = options.update(width=code_width)733 734 highlight_line = self.highlight_lines.__contains__735 _Segment = Segment736 new_line = _Segment("\n")737 738 line_pointer = "> " if options.legacy_windows else "❱ "739 740 (741 background_style,742 number_style,743 highlight_number_style,744 ) = self._get_number_styles(console)745 746 for line_no, line in enumerate(lines, self.start_line + line_offset):747 if self.word_wrap:748 wrapped_lines = console.render_lines(749 line,750 render_options.update(height=None, justify="left"),751 style=background_style,752 pad=not transparent_background,753 )754 else:755 segments = list(line.render(console, end=""))756 if options.no_wrap:757 wrapped_lines = [segments]758 else:759 wrapped_lines = [760 _Segment.adjust_line_length(761 segments,762 render_options.max_width,763 style=background_style,764 pad=not transparent_background,765 )766 ]767 768 if self.line_numbers:769 wrapped_line_left_pad = _Segment(770 " " * numbers_column_width + " ", background_style771 )772 for first, wrapped_line in loop_first(wrapped_lines):773 if first:774 line_column = str(line_no).rjust(numbers_column_width - 2) + " "775 if highlight_line(line_no):776 yield _Segment(line_pointer, Style(color="red"))777 yield _Segment(line_column, highlight_number_style)778 else:779 yield _Segment(" ", highlight_number_style)780 yield _Segment(line_column, number_style)781 else:782 yield wrapped_line_left_pad783 yield from wrapped_line784 yield new_line785 else:786 for wrapped_line in wrapped_lines:787 yield from wrapped_line788 yield new_line789 790 def _apply_stylized_ranges(self, text: Text) -> None:791 """792 Apply stylized ranges to a text instance,793 using the given code to determine the right portion to apply the style to.794 795 Args:796 text (Text): Text instance to apply the style to.797 """798 code = text.plain799 newlines_offsets = [800 # Let's add outer boundaries at each side of the list:801 0,802 # N.B. using "\n" here is much faster than using metacharacters such as "^" or "\Z":803 *[804 match.start() + 1805 for match in re.finditer("\n", code, flags=re.MULTILINE)806 ],807 len(code) + 1,808 ]809 810 for stylized_range in self._stylized_ranges:811 start = _get_code_index_for_syntax_position(812 newlines_offsets, stylized_range.start813 )814 end = _get_code_index_for_syntax_position(815 newlines_offsets, stylized_range.end816 )817 if start is not None and end is not None:818 if stylized_range.style_before:819 text.stylize_before(stylized_range.style, start, end)820 else:821 text.stylize(stylized_range.style, start, end)822 823 def _process_code(self, code: str) -> Tuple[bool, str]:824 """825 Applies various processing to a raw code string826 (normalises it so it always ends with a line return, dedents it if necessary, etc.)827 828 Args:829 code (str): The raw code string to process830 831 Returns:832 Tuple[bool, str]: the boolean indicates whether the raw code ends with a line return,833 while the string is the processed code.834 """835 ends_on_nl = code.endswith("\n")836 processed_code = code if ends_on_nl else code + "\n"837 processed_code = (838 textwrap.dedent(processed_code) if self.dedent else processed_code839 )840 processed_code = processed_code.expandtabs(self.tab_size)841 return ends_on_nl, processed_code842 843 844def _get_code_index_for_syntax_position(845 newlines_offsets: Sequence[int], position: SyntaxPosition846) -> Optional[int]:847 """848 Returns the index of the code string for the given positions.849 850 Args:851 newlines_offsets (Sequence[int]): The offset of each newline character found in the code snippet.852 position (SyntaxPosition): The position to search for.853 854 Returns:855 Optional[int]: The index of the code string for this position, or `None`856 if the given position's line number is out of range (if it's the column that is out of range857 we silently clamp its value so that it reaches the end of the line)858 """859 lines_count = len(newlines_offsets)860 861 line_number, column_index = position862 if line_number > lines_count or len(newlines_offsets) < (line_number + 1):863 return None # `line_number` is out of range864 line_index = line_number - 1865 line_length = newlines_offsets[line_index + 1] - newlines_offsets[line_index] - 1866 # If `column_index` is out of range: let's silently clamp it:867 column_index = min(line_length, column_index)868 return newlines_offsets[line_index] + column_index869 870 871if __name__ == "__main__": # pragma: no cover872 import argparse873 import sys874 875 parser = argparse.ArgumentParser(876 description="Render syntax to the console with Rich"877 )878 parser.add_argument(879 "path",880 metavar="PATH",881 help="path to file, or - for stdin",882 )883 parser.add_argument(884 "-c",885 "--force-color",886 dest="force_color",887 action="store_true",888 default=None,889 help="force color for non-terminals",890 )891 parser.add_argument(892 "-i",893 "--indent-guides",894 dest="indent_guides",895 action="store_true",896 default=False,897 help="display indent guides",898 )899 parser.add_argument(900 "-l",901 "--line-numbers",902 dest="line_numbers",903 action="store_true",904 help="render line numbers",905 )906 parser.add_argument(907 "-w",908 "--width",909 type=int,910 dest="width",911 default=None,912 help="width of output (default will auto-detect)",913 )914 parser.add_argument(915 "-r",916 "--wrap",917 dest="word_wrap",918 action="store_true",919 default=False,920 help="word wrap long lines",921 )922 parser.add_argument(923 "-s",924 "--soft-wrap",925 action="store_true",926 dest="soft_wrap",927 default=False,928 help="enable soft wrapping mode",929 )930 parser.add_argument(931 "-t", "--theme", dest="theme", default="monokai", help="pygments theme"932 )933 parser.add_argument(934 "-b",935 "--background-color",936 dest="background_color",937 default=None,938 help="Override background color",939 )940 parser.add_argument(941 "-x",942 "--lexer",943 default=None,944 dest="lexer_name",945 help="Lexer name",946 )947 parser.add_argument(948 "-p", "--padding", type=int, default=0, dest="padding", help="Padding"949 )950 parser.add_argument(951 "--highlight-line",952 type=int,953 default=None,954 dest="highlight_line",955 help="The line number (not index!) to highlight",956 )957 args = parser.parse_args()958 959 from rich.console import Console960 961 console = Console(force_terminal=args.force_color, width=args.width)962 963 if args.path == "-":964 code = sys.stdin.read()965 syntax = Syntax(966 code=code,967 lexer=args.lexer_name,968 line_numbers=args.line_numbers,969 word_wrap=args.word_wrap,970 theme=args.theme,971 background_color=args.background_color,972 indent_guides=args.indent_guides,973 padding=args.padding,974 highlight_lines={args.highlight_line},975 )976 else:977 syntax = Syntax.from_path(978 args.path,979 lexer=args.lexer_name,980 line_numbers=args.line_numbers,981 word_wrap=args.word_wrap,982 theme=args.theme,983 background_color=args.background_color,984 indent_guides=args.indent_guides,985 padding=args.padding,986 highlight_lines={args.highlight_line},987 )988 console.print(syntax, soft_wrap=args.soft_wrap)989 