Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
syntax.py989 linesDownload Raw Back to rich
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 
codekingpro/portable-devtools · Team Ai