Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rich/syntax.py: 57%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import os.path
4import re
5import sys
6import textwrap
7from abc import ABC, abstractmethod
8from pathlib import Path
9from typing import (
10 Any,
11 Dict,
12 Iterable,
13 List,
14 NamedTuple,
15 Optional,
16 Sequence,
17 Set,
18 Tuple,
19 Type,
20 Union,
21)
23from pygments.lexer import Lexer
24from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename
25from pygments.style import Style as PygmentsStyle
26from pygments.styles import get_style_by_name
27from pygments.token import (
28 Comment,
29 Error,
30 Generic,
31 Keyword,
32 Name,
33 Number,
34 Operator,
35 String,
36 Token,
37 Whitespace,
38)
39from pygments.util import ClassNotFound
41from rich.containers import Lines
42from rich.padding import Padding, PaddingDimensions
44from ._loop import loop_first
45from .cells import cell_len
46from .color import Color, blend_rgb
47from .console import Console, ConsoleOptions, JustifyMethod, RenderResult
48from .jupyter import JupyterMixin
49from .measure import Measurement
50from .segment import Segment, Segments
51from .style import Style, StyleType
52from .text import Text
54TokenType = Tuple[str, ...]
56WINDOWS = sys.platform == "win32"
57DEFAULT_THEME = "monokai"
59# The following styles are based on https://github.com/pygments/pygments/blob/master/pygments/formatters/terminal.py
60# A few modifications were made
62ANSI_LIGHT: Dict[TokenType, Style] = {
63 Token: Style(),
64 Whitespace: Style(color="white"),
65 Comment: Style(dim=True),
66 Comment.Preproc: Style(color="cyan"),
67 Keyword: Style(color="blue"),
68 Keyword.Type: Style(color="cyan"),
69 Operator.Word: Style(color="magenta"),
70 Name.Builtin: Style(color="cyan"),
71 Name.Function: Style(color="green"),
72 Name.Namespace: Style(color="cyan", underline=True),
73 Name.Class: Style(color="green", underline=True),
74 Name.Exception: Style(color="cyan"),
75 Name.Decorator: Style(color="magenta", bold=True),
76 Name.Variable: Style(color="red"),
77 Name.Constant: Style(color="red"),
78 Name.Attribute: Style(color="cyan"),
79 Name.Tag: Style(color="bright_blue"),
80 String: Style(color="yellow"),
81 Number: Style(color="blue"),
82 Generic.Deleted: Style(color="bright_red"),
83 Generic.Inserted: Style(color="green"),
84 Generic.Heading: Style(bold=True),
85 Generic.Subheading: Style(color="magenta", bold=True),
86 Generic.Prompt: Style(bold=True),
87 Generic.Error: Style(color="bright_red"),
88 Error: Style(color="red", underline=True),
89}
91ANSI_DARK: Dict[TokenType, Style] = {
92 Token: Style(),
93 Whitespace: Style(color="bright_black"),
94 Comment: Style(dim=True),
95 Comment.Preproc: Style(color="bright_cyan"),
96 Keyword: Style(color="bright_blue"),
97 Keyword.Type: Style(color="bright_cyan"),
98 Operator.Word: Style(color="bright_magenta"),
99 Name.Builtin: Style(color="bright_cyan"),
100 Name.Function: Style(color="bright_green"),
101 Name.Namespace: Style(color="bright_cyan", underline=True),
102 Name.Class: Style(color="bright_green", underline=True),
103 Name.Exception: Style(color="bright_cyan"),
104 Name.Decorator: Style(color="bright_magenta", bold=True),
105 Name.Variable: Style(color="bright_red"),
106 Name.Constant: Style(color="bright_red"),
107 Name.Attribute: Style(color="bright_cyan"),
108 Name.Tag: Style(color="bright_blue"),
109 String: Style(color="yellow"),
110 Number: Style(color="bright_blue"),
111 Generic.Deleted: Style(color="bright_red"),
112 Generic.Inserted: Style(color="bright_green"),
113 Generic.Heading: Style(bold=True),
114 Generic.Subheading: Style(color="bright_magenta", bold=True),
115 Generic.Prompt: Style(bold=True),
116 Generic.Error: Style(color="bright_red"),
117 Error: Style(color="red", underline=True),
118}
120RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK}
121NUMBERS_COLUMN_DEFAULT_PADDING = 2
124class SyntaxTheme(ABC):
125 """Base class for a syntax theme."""
127 @abstractmethod
128 def get_style_for_token(self, token_type: TokenType) -> Style:
129 """Get a style for a given Pygments token."""
130 raise NotImplementedError # pragma: no cover
132 @abstractmethod
133 def get_background_style(self) -> Style:
134 """Get the background color."""
135 raise NotImplementedError # pragma: no cover
138class PygmentsSyntaxTheme(SyntaxTheme):
139 """Syntax theme that delegates to Pygments theme."""
141 def __init__(self, theme: Union[str, Type[PygmentsStyle]]) -> None:
142 self._style_cache: Dict[TokenType, Style] = {}
143 if isinstance(theme, str):
144 try:
145 self._pygments_style_class = get_style_by_name(theme)
146 except ClassNotFound:
147 self._pygments_style_class = get_style_by_name("default")
148 else:
149 self._pygments_style_class = theme
151 self._background_color = self._pygments_style_class.background_color
152 self._background_style = Style(bgcolor=self._background_color)
154 def get_style_for_token(self, token_type: TokenType) -> Style:
155 """Get a style from a Pygments class."""
156 try:
157 return self._style_cache[token_type]
158 except KeyError:
159 try:
160 pygments_style = self._pygments_style_class.style_for_token(token_type)
161 except KeyError:
162 style = Style.null()
163 else:
164 color = pygments_style["color"]
165 bgcolor = pygments_style["bgcolor"]
166 style = Style(
167 color="#" + color if color else "#000000",
168 bgcolor="#" + bgcolor if bgcolor else self._background_color,
169 bold=pygments_style["bold"],
170 italic=pygments_style["italic"],
171 underline=pygments_style["underline"],
172 )
173 self._style_cache[token_type] = style
174 return style
176 def get_background_style(self) -> Style:
177 return self._background_style
180class ANSISyntaxTheme(SyntaxTheme):
181 """Syntax theme to use standard colors."""
183 def __init__(self, style_map: Dict[TokenType, Style]) -> None:
184 self.style_map = style_map
185 self._missing_style = Style.null()
186 self._background_style = Style.null()
187 self._style_cache: Dict[TokenType, Style] = {}
189 def get_style_for_token(self, token_type: TokenType) -> Style:
190 """Look up style in the style map."""
191 try:
192 return self._style_cache[token_type]
193 except KeyError:
194 # Styles form a hierarchy
195 # We need to go from most to least specific
196 # e.g. ("foo", "bar", "baz") to ("foo", "bar") to ("foo",)
197 get_style = self.style_map.get
198 token = tuple(token_type)
199 style = self._missing_style
200 while token:
201 _style = get_style(token)
202 if _style is not None:
203 style = _style
204 break
205 token = token[:-1]
206 self._style_cache[token_type] = style
207 return style
209 def get_background_style(self) -> Style:
210 return self._background_style
213SyntaxPosition = Tuple[int, int]
216class _SyntaxHighlightRange(NamedTuple):
217 """
218 A range to highlight in a Syntax object.
219 `start` and `end` are 2-integers tuples, where the first integer is the line number
220 (starting from 1) and the second integer is the column index (starting from 0).
221 """
223 style: StyleType
224 start: SyntaxPosition
225 end: SyntaxPosition
226 style_before: bool = False
229class PaddingProperty:
230 """Descriptor to get and set padding."""
232 def __get__(self, obj: Syntax, objtype: Type[Syntax]) -> Tuple[int, int, int, int]:
233 """Space around the Syntax."""
234 return obj._padding
236 def __set__(self, obj: Syntax, padding: PaddingDimensions) -> None:
237 obj._padding = Padding.unpack(padding)
240class Syntax(JupyterMixin):
241 """Construct a Syntax object to render syntax highlighted code.
243 Args:
244 code (str): Code to highlight.
245 lexer (Lexer | str): Lexer to use (see https://pygments.org/docs/lexers/)
246 theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "monokai".
247 dedent (bool, optional): Enable stripping of initial whitespace. Defaults to False.
248 line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.
249 start_line (int, optional): Starting number for line numbers. Defaults to 1.
250 line_range (Tuple[int | None, int | None], optional): If given should be a tuple of the start and end line to render.
251 A value of None in the tuple indicates the range is open in that direction.
252 highlight_lines (Set[int]): A set of line numbers to highlight.
253 code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.
254 tab_size (int, optional): Size of tabs. Defaults to 4.
255 word_wrap (bool, optional): Enable word wrapping.
256 background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
257 indent_guides (bool, optional): Show indent guides. Defaults to False.
258 padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).
259 """
261 _pygments_style_class: Type[PygmentsStyle]
262 _theme: SyntaxTheme
264 @classmethod
265 def get_theme(cls, name: Union[str, SyntaxTheme]) -> SyntaxTheme:
266 """Get a syntax theme instance."""
267 if isinstance(name, SyntaxTheme):
268 return name
269 theme: SyntaxTheme
270 if name in RICH_SYNTAX_THEMES:
271 theme = ANSISyntaxTheme(RICH_SYNTAX_THEMES[name])
272 else:
273 theme = PygmentsSyntaxTheme(name)
274 return theme
276 def __init__(
277 self,
278 code: str,
279 lexer: Union[Lexer, str],
280 *,
281 theme: Union[str, SyntaxTheme] = DEFAULT_THEME,
282 dedent: bool = False,
283 line_numbers: bool = False,
284 start_line: int = 1,
285 line_range: Optional[Tuple[Optional[int], Optional[int]]] = None,
286 highlight_lines: Optional[Set[int]] = None,
287 code_width: Optional[int] = None,
288 tab_size: int = 4,
289 word_wrap: bool = False,
290 background_color: Optional[str] = None,
291 indent_guides: bool = False,
292 padding: PaddingDimensions = 0,
293 ) -> None:
294 self.code = code
295 self._lexer = lexer
296 self.dedent = dedent
297 self.line_numbers = line_numbers
298 self.start_line = start_line
299 self.line_range = line_range
300 self.highlight_lines = highlight_lines or set()
301 self.code_width = code_width
302 self.tab_size = tab_size
303 self.word_wrap = word_wrap
304 self.background_color = background_color
305 self.background_style = (
306 Style(bgcolor=background_color) if background_color else Style()
307 )
308 self.indent_guides = indent_guides
309 self._padding = Padding.unpack(padding)
311 self._theme = self.get_theme(theme)
312 self._stylized_ranges: List[_SyntaxHighlightRange] = []
314 padding = PaddingProperty()
316 @classmethod
317 def from_path(
318 cls,
319 path: str,
320 encoding: str = "utf-8",
321 lexer: Optional[Union[Lexer, str]] = None,
322 theme: Union[str, SyntaxTheme] = DEFAULT_THEME,
323 dedent: bool = False,
324 line_numbers: bool = False,
325 line_range: Optional[Tuple[int, int]] = None,
326 start_line: int = 1,
327 highlight_lines: Optional[Set[int]] = None,
328 code_width: Optional[int] = None,
329 tab_size: int = 4,
330 word_wrap: bool = False,
331 background_color: Optional[str] = None,
332 indent_guides: bool = False,
333 padding: PaddingDimensions = 0,
334 ) -> "Syntax":
335 """Construct a Syntax object from a file.
337 Args:
338 path (str): Path to file to highlight.
339 encoding (str): Encoding of file.
340 lexer (str | Lexer, optional): Lexer to use. If None, lexer will be auto-detected from path/file content.
341 theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "emacs".
342 dedent (bool, optional): Enable stripping of initial whitespace. Defaults to True.
343 line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.
344 start_line (int, optional): Starting number for line numbers. Defaults to 1.
345 line_range (Tuple[int, int], optional): If given should be a tuple of the start and end line to render.
346 highlight_lines (Set[int]): A set of line numbers to highlight.
347 code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.
348 tab_size (int, optional): Size of tabs. Defaults to 4.
349 word_wrap (bool, optional): Enable word wrapping of code.
350 background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
351 indent_guides (bool, optional): Show indent guides. Defaults to False.
352 padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).
354 Returns:
355 [Syntax]: A Syntax object that may be printed to the console
356 """
357 code = Path(path).read_text(encoding=encoding)
359 if not lexer:
360 lexer = cls.guess_lexer(path, code=code)
362 return cls(
363 code,
364 lexer,
365 theme=theme,
366 dedent=dedent,
367 line_numbers=line_numbers,
368 line_range=line_range,
369 start_line=start_line,
370 highlight_lines=highlight_lines,
371 code_width=code_width,
372 tab_size=tab_size,
373 word_wrap=word_wrap,
374 background_color=background_color,
375 indent_guides=indent_guides,
376 padding=padding,
377 )
379 @classmethod
380 def guess_lexer(cls, path: str, code: Optional[str] = None) -> str:
381 """Guess the alias of the Pygments lexer to use based on a path and an optional string of code.
382 If code is supplied, it will use a combination of the code and the filename to determine the
383 best lexer to use. For example, if the file is ``index.html`` and the file contains Django
384 templating syntax, then "html+django" will be returned. If the file is ``index.html``, and no
385 templating language is used, the "html" lexer will be used. If no string of code
386 is supplied, the lexer will be chosen based on the file extension..
388 Args:
389 path (AnyStr): The path to the file containing the code you wish to know the lexer for.
390 code (str, optional): Optional string of code that will be used as a fallback if no lexer
391 is found for the supplied path.
393 Returns:
394 str: The name of the Pygments lexer that best matches the supplied path/code.
395 """
396 lexer: Optional[Lexer] = None
397 lexer_name = "default"
398 if code:
399 try:
400 lexer = guess_lexer_for_filename(path, code)
401 except ClassNotFound:
402 pass
404 if not lexer:
405 try:
406 _, ext = os.path.splitext(path)
407 if ext:
408 extension = ext.lstrip(".").lower()
409 lexer = get_lexer_by_name(extension)
410 except ClassNotFound:
411 pass
413 if lexer:
414 if lexer.aliases:
415 lexer_name = lexer.aliases[0]
416 else:
417 lexer_name = lexer.name
419 return lexer_name
421 def _get_base_style(self) -> Style:
422 """Get the base style."""
423 default_style = self._theme.get_background_style() + self.background_style
424 return default_style
426 def _get_token_color(self, token_type: TokenType) -> Optional[Color]:
427 """Get a color (if any) for the given token.
429 Args:
430 token_type (TokenType): A token type tuple from Pygments.
432 Returns:
433 Optional[Color]: Color from theme, or None for no color.
434 """
435 style = self._theme.get_style_for_token(token_type)
436 return style.color
438 @property
439 def lexer(self) -> Optional[Lexer]:
440 """The lexer for this syntax, or None if no lexer was found.
442 Tries to find the lexer by name if a string was passed to the constructor.
443 """
445 if isinstance(self._lexer, Lexer):
446 return self._lexer
447 try:
448 return get_lexer_by_name(
449 self._lexer,
450 stripnl=False,
451 ensurenl=True,
452 tabsize=self.tab_size,
453 )
454 except ClassNotFound:
455 return None
457 @property
458 def default_lexer(self) -> Lexer:
459 """A Pygments Lexer to use if one is not specified or invalid."""
460 return get_lexer_by_name(
461 "text",
462 stripnl=False,
463 ensurenl=True,
464 tabsize=self.tab_size,
465 )
467 def highlight(
468 self,
469 code: str,
470 line_range: Optional[Tuple[Optional[int], Optional[int]]] = None,
471 ) -> Text:
472 """Highlight code and return a Text instance.
474 Args:
475 code (str): Code to highlight.
476 line_range(Tuple[int, int], optional): Optional line range to highlight.
478 Returns:
479 Text: A text instance containing highlighted syntax.
480 """
482 base_style = self._get_base_style()
483 justify: JustifyMethod = (
484 "default" if base_style.transparent_background else "left"
485 )
487 text = Text(
488 justify=justify,
489 style=base_style,
490 tab_size=self.tab_size,
491 no_wrap=not self.word_wrap,
492 )
493 _get_theme_style = self._theme.get_style_for_token
495 lexer = self.lexer or self.default_lexer
497 if lexer is None:
498 text.append(code)
499 else:
500 if line_range:
501 # More complicated path to only stylize a portion of the code
502 # This speeds up further operations as there are less spans to process
503 line_start, line_end = line_range
505 def line_tokenize() -> Iterable[Tuple[Any, str]]:
506 """Split tokens to one per line."""
507 assert lexer # required to make MyPy happy - we know lexer is not None at this point
509 for token_type, token in lexer.get_tokens(code):
510 while token:
511 line_token, new_line, token = token.partition("\n")
512 yield token_type, line_token + new_line
514 def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]:
515 """Convert tokens to spans."""
516 tokens = iter(line_tokenize())
517 line_no = 0
518 _line_start = line_start - 1 if line_start else 0
520 # Skip over tokens until line start
521 while line_no < _line_start:
522 try:
523 _token_type, token = next(tokens)
524 except StopIteration:
525 break
526 yield (token, None)
527 if token.endswith("\n"):
528 line_no += 1
529 # Generate spans until line end
530 for token_type, token in tokens:
531 yield (token, _get_theme_style(token_type))
532 if token.endswith("\n"):
533 line_no += 1
534 if line_end and line_no >= line_end:
535 break
537 text.append_tokens(tokens_to_spans())
539 else:
540 text.append_tokens(
541 (token, _get_theme_style(token_type))
542 for token_type, token in lexer.get_tokens(code)
543 )
544 if self.background_color is not None:
545 text.stylize(f"on {self.background_color}")
547 if self._stylized_ranges:
548 self._apply_stylized_ranges(text)
550 return text
552 def stylize_range(
553 self,
554 style: StyleType,
555 start: SyntaxPosition,
556 end: SyntaxPosition,
557 style_before: bool = False,
558 ) -> None:
559 """
560 Adds a custom style on a part of the code, that will be applied to the syntax display when it's rendered.
561 Line numbers are 1-based, while column indexes are 0-based.
563 Args:
564 style (StyleType): The style to apply.
565 start (Tuple[int, int]): The start of the range, in the form `[line number, column index]`.
566 end (Tuple[int, int]): The end of the range, in the form `[line number, column index]`.
567 style_before (bool): Apply the style before any existing styles.
568 """
569 self._stylized_ranges.append(
570 _SyntaxHighlightRange(style, start, end, style_before)
571 )
573 def _get_line_numbers_color(self, blend: float = 0.3) -> Color:
574 background_style = self._theme.get_background_style() + self.background_style
575 background_color = background_style.bgcolor
576 if background_color is None or background_color.is_system_defined:
577 return Color.default()
578 foreground_color = self._get_token_color(Token.Text)
579 if foreground_color is None or foreground_color.is_system_defined:
580 return foreground_color or Color.default()
581 new_color = blend_rgb(
582 background_color.get_truecolor(),
583 foreground_color.get_truecolor(),
584 cross_fade=blend,
585 )
586 return Color.from_triplet(new_color)
588 @property
589 def _numbers_column_width(self) -> int:
590 """Get the number of characters used to render the numbers column."""
591 column_width = 0
592 if self.line_numbers:
593 column_width = (
594 len(str(self.start_line + self.code.count("\n")))
595 + NUMBERS_COLUMN_DEFAULT_PADDING
596 )
597 return column_width
599 def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]:
600 """Get background, number, and highlight styles for line numbers."""
601 background_style = self._get_base_style()
602 if background_style.transparent_background:
603 return Style.null(), Style(dim=True), Style.null()
604 if console.color_system in ("256", "truecolor"):
605 number_style = Style.chain(
606 background_style,
607 self._theme.get_style_for_token(Token.Text),
608 Style(color=self._get_line_numbers_color()),
609 self.background_style,
610 )
611 highlight_number_style = Style.chain(
612 background_style,
613 self._theme.get_style_for_token(Token.Text),
614 Style(bold=True, color=self._get_line_numbers_color(0.9)),
615 self.background_style,
616 )
617 else:
618 number_style = background_style + Style(dim=True)
619 highlight_number_style = background_style + Style(dim=False)
620 return background_style, number_style, highlight_number_style
622 def __rich_measure__(
623 self, console: "Console", options: "ConsoleOptions"
624 ) -> "Measurement":
625 _, right, _, left = self.padding
626 padding = left + right
627 if self.code_width is not None:
628 width = self.code_width + self._numbers_column_width + padding + 1
629 return Measurement(self._numbers_column_width, width)
630 lines = self.code.splitlines()
631 width = (
632 self._numbers_column_width
633 + padding
634 + (max(cell_len(line) for line in lines) if lines else 0)
635 )
636 if self.line_numbers:
637 width += 1
638 return Measurement(self._numbers_column_width, width)
640 def __rich_console__(
641 self, console: Console, options: ConsoleOptions
642 ) -> RenderResult:
643 segments = Segments(self._get_syntax(console, options))
644 if any(self.padding):
645 yield Padding(segments, style=self._get_base_style(), pad=self.padding)
646 else:
647 yield segments
649 def _get_syntax(
650 self,
651 console: Console,
652 options: ConsoleOptions,
653 ) -> Iterable[Segment]:
654 """
655 Get the Segments for the Syntax object, excluding any vertical/horizontal padding
656 """
657 transparent_background = self._get_base_style().transparent_background
658 _pad_top, pad_right, _pad_bottom, pad_left = self.padding
659 horizontal_padding = pad_left + pad_right
660 code_width = (
661 (
662 (options.max_width - self._numbers_column_width - 1)
663 if self.line_numbers
664 else options.max_width
665 )
666 - horizontal_padding
667 if self.code_width is None
668 else self.code_width
669 )
670 code_width = max(0, code_width)
672 ends_on_nl, processed_code = self._process_code(self.code)
673 text = self.highlight(processed_code, self.line_range)
675 if not self.line_numbers and not self.word_wrap and not self.line_range:
676 if not ends_on_nl:
677 text.remove_suffix("\n")
678 # Simple case of just rendering text
679 style = (
680 self._get_base_style()
681 + self._theme.get_style_for_token(Comment)
682 + Style(dim=True)
683 + self.background_style
684 )
685 if self.indent_guides and not options.ascii_only:
686 text = text.with_indent_guides(self.tab_size, style=style)
687 text.overflow = "crop"
688 if style.transparent_background:
689 yield from console.render(
690 text, options=options.update(width=code_width)
691 )
692 else:
693 syntax_lines = console.render_lines(
694 text,
695 options.update(width=code_width, height=None, justify="left"),
696 style=self.background_style,
697 pad=True,
698 new_lines=True,
699 )
700 for syntax_line in syntax_lines:
701 yield from syntax_line
702 return
704 start_line, end_line = self.line_range or (None, None)
705 line_offset = 0
706 if start_line:
707 line_offset = max(0, start_line - 1)
708 lines: Union[List[Text], Lines] = text.split("\n", allow_blank=ends_on_nl)
709 if self.line_range:
710 if line_offset > len(lines):
711 return
712 lines = lines[line_offset:end_line]
714 if self.indent_guides and not options.ascii_only:
715 style = (
716 self._get_base_style()
717 + self._theme.get_style_for_token(Comment)
718 + Style(dim=True)
719 + self.background_style
720 )
721 lines = (
722 Text("\n")
723 .join(lines)
724 .with_indent_guides(self.tab_size, style=style + Style(italic=False))
725 .split("\n", allow_blank=True)
726 )
728 numbers_column_width = self._numbers_column_width
729 render_options = options.update(width=code_width)
731 highlight_line = self.highlight_lines.__contains__
732 _Segment = Segment
733 new_line = _Segment("\n")
735 line_pointer = "> " if options.legacy_windows else "❱ "
737 (
738 background_style,
739 number_style,
740 highlight_number_style,
741 ) = self._get_number_styles(console)
743 for line_no, line in enumerate(lines, self.start_line + line_offset):
744 if self.word_wrap:
745 wrapped_lines = console.render_lines(
746 line,
747 render_options.update(height=None, justify="left"),
748 style=background_style,
749 pad=not transparent_background,
750 )
751 else:
752 segments = list(line.render(console, end=""))
753 if options.no_wrap:
754 wrapped_lines = [segments]
755 else:
756 wrapped_lines = [
757 _Segment.adjust_line_length(
758 segments,
759 render_options.max_width,
760 style=background_style,
761 pad=not transparent_background,
762 )
763 ]
765 if self.line_numbers:
766 wrapped_line_left_pad = _Segment(
767 " " * numbers_column_width + " ", background_style
768 )
769 for first, wrapped_line in loop_first(wrapped_lines):
770 if first:
771 line_column = str(line_no).rjust(numbers_column_width - 2) + " "
772 if highlight_line(line_no):
773 yield _Segment(line_pointer, Style(color="red"))
774 yield _Segment(line_column, highlight_number_style)
775 else:
776 yield _Segment(" ", highlight_number_style)
777 yield _Segment(line_column, number_style)
778 else:
779 yield wrapped_line_left_pad
780 yield from wrapped_line
781 yield new_line
782 else:
783 for wrapped_line in wrapped_lines:
784 yield from wrapped_line
785 yield new_line
787 def _apply_stylized_ranges(self, text: Text) -> None:
788 """
789 Apply stylized ranges to a text instance,
790 using the given code to determine the right portion to apply the style to.
792 Args:
793 text (Text): Text instance to apply the style to.
794 """
795 code = text.plain
796 newlines_offsets = [
797 # Let's add outer boundaries at each side of the list:
798 0,
799 # N.B. using "\n" here is much faster than using metacharacters such as "^" or "\Z":
800 *[
801 match.start() + 1
802 for match in re.finditer("\n", code, flags=re.MULTILINE)
803 ],
804 len(code) + 1,
805 ]
807 for stylized_range in self._stylized_ranges:
808 start = _get_code_index_for_syntax_position(
809 newlines_offsets, stylized_range.start
810 )
811 end = _get_code_index_for_syntax_position(
812 newlines_offsets, stylized_range.end
813 )
814 if start is not None and end is not None:
815 if stylized_range.style_before:
816 text.stylize_before(stylized_range.style, start, end)
817 else:
818 text.stylize(stylized_range.style, start, end)
820 def _process_code(self, code: str) -> Tuple[bool, str]:
821 """
822 Applies various processing to a raw code string
823 (normalises it so it always ends with a line return, dedents it if necessary, etc.)
825 Args:
826 code (str): The raw code string to process
828 Returns:
829 Tuple[bool, str]: the boolean indicates whether the raw code ends with a line return,
830 while the string is the processed code.
831 """
832 ends_on_nl = code.endswith("\n")
833 processed_code = code if ends_on_nl else code + "\n"
834 processed_code = (
835 textwrap.dedent(processed_code) if self.dedent else processed_code
836 )
837 processed_code = processed_code.expandtabs(self.tab_size)
838 return ends_on_nl, processed_code
841def _get_code_index_for_syntax_position(
842 newlines_offsets: Sequence[int], position: SyntaxPosition
843) -> Optional[int]:
844 """
845 Returns the index of the code string for the given positions.
847 Args:
848 newlines_offsets (Sequence[int]): The offset of each newline character found in the code snippet.
849 position (SyntaxPosition): The position to search for.
851 Returns:
852 Optional[int]: The index of the code string for this position, or `None`
853 if the given position's line number is out of range (if it's the column that is out of range
854 we silently clamp its value so that it reaches the end of the line)
855 """
856 lines_count = len(newlines_offsets)
858 line_number, column_index = position
859 if line_number > lines_count or len(newlines_offsets) < (line_number + 1):
860 return None # `line_number` is out of range
861 line_index = line_number - 1
862 line_length = newlines_offsets[line_index + 1] - newlines_offsets[line_index] - 1
863 # If `column_index` is out of range: let's silently clamp it:
864 column_index = min(line_length, column_index)
865 return newlines_offsets[line_index] + column_index
868if __name__ == "__main__": # pragma: no cover
869 import argparse
870 import sys
872 parser = argparse.ArgumentParser(
873 description="Render syntax to the console with Rich"
874 )
875 parser.add_argument(
876 "path",
877 metavar="PATH",
878 help="path to file, or - for stdin",
879 )
880 parser.add_argument(
881 "-c",
882 "--force-color",
883 dest="force_color",
884 action="store_true",
885 default=None,
886 help="force color for non-terminals",
887 )
888 parser.add_argument(
889 "-i",
890 "--indent-guides",
891 dest="indent_guides",
892 action="store_true",
893 default=False,
894 help="display indent guides",
895 )
896 parser.add_argument(
897 "-l",
898 "--line-numbers",
899 dest="line_numbers",
900 action="store_true",
901 help="render line numbers",
902 )
903 parser.add_argument(
904 "-w",
905 "--width",
906 type=int,
907 dest="width",
908 default=None,
909 help="width of output (default will auto-detect)",
910 )
911 parser.add_argument(
912 "-r",
913 "--wrap",
914 dest="word_wrap",
915 action="store_true",
916 default=False,
917 help="word wrap long lines",
918 )
919 parser.add_argument(
920 "-s",
921 "--soft-wrap",
922 action="store_true",
923 dest="soft_wrap",
924 default=False,
925 help="enable soft wrapping mode",
926 )
927 parser.add_argument(
928 "-t", "--theme", dest="theme", default="monokai", help="pygments theme"
929 )
930 parser.add_argument(
931 "-b",
932 "--background-color",
933 dest="background_color",
934 default=None,
935 help="Override background color",
936 )
937 parser.add_argument(
938 "-x",
939 "--lexer",
940 default=None,
941 dest="lexer_name",
942 help="Lexer name",
943 )
944 parser.add_argument(
945 "-p", "--padding", type=int, default=0, dest="padding", help="Padding"
946 )
947 parser.add_argument(
948 "--highlight-line",
949 type=int,
950 default=None,
951 dest="highlight_line",
952 help="The line number (not index!) to highlight",
953 )
954 args = parser.parse_args()
956 from rich.console import Console
958 console = Console(force_terminal=args.force_color, width=args.width)
960 if args.path == "-":
961 code = sys.stdin.read()
962 syntax = Syntax(
963 code=code,
964 lexer=args.lexer_name,
965 line_numbers=args.line_numbers,
966 word_wrap=args.word_wrap,
967 theme=args.theme,
968 background_color=args.background_color,
969 indent_guides=args.indent_guides,
970 padding=args.padding,
971 highlight_lines={args.highlight_line},
972 )
973 else:
974 syntax = Syntax.from_path(
975 args.path,
976 lexer=args.lexer_name,
977 line_numbers=args.line_numbers,
978 word_wrap=args.word_wrap,
979 theme=args.theme,
980 background_color=args.background_color,
981 indent_guides=args.indent_guides,
982 padding=args.padding,
983 highlight_lines={args.highlight_line},
984 )
985 console.print(syntax, soft_wrap=args.soft_wrap)