Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rich/syntax.py: 24%
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 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)
24from pygments.lexer import Lexer
25from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename
26from pygments.style import Style as PygmentsStyle
27from pygments.styles import get_style_by_name
28from 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 ClassNotFound
42if TYPE_CHECKING:
43 from .console import Console, ConsoleOptions, JustifyMethod, RenderResult
45from rich.containers import Lines
46from rich.padding import Padding, PaddingDimensions
48from ._loop import loop_first
49from .cells import cell_len
50from .color import Color, blend_rgb
51from .jupyter import JupyterMixin
52from .measure import Measurement
53from .segment import Segment, Segments
54from .style import Style, StyleType
55from .text import Text
57TokenType = Tuple[str, ...]
59WINDOWS = sys.platform == "win32"
60DEFAULT_THEME = "monokai"
62# The following styles are based on https://github.com/pygments/pygments/blob/master/pygments/formatters/terminal.py
63# A few modifications were made
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}
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}
123RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK}
124NUMBERS_COLUMN_DEFAULT_PADDING = 2
127class SyntaxTheme(ABC):
128 """Base class for a syntax theme."""
130 @abstractmethod
131 def get_style_for_token(self, token_type: TokenType) -> Style:
132 """Get a style for a given Pygments token."""
133 raise NotImplementedError # pragma: no cover
135 @abstractmethod
136 def get_background_style(self) -> Style:
137 """Get the background color."""
138 raise NotImplementedError # pragma: no cover
141class PygmentsSyntaxTheme(SyntaxTheme):
142 """Syntax theme that delegates to Pygments theme."""
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 = theme
154 self._background_color = self._pygments_style_class.background_color
155 self._background_style = Style(bgcolor=self._background_color)
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] = style
177 return style
179 def get_background_style(self) -> Style:
180 return self._background_style
183class ANSISyntaxTheme(SyntaxTheme):
184 """Syntax theme to use standard colors."""
186 def __init__(self, style_map: Dict[TokenType, Style]) -> None:
187 self.style_map = style_map
188 self._missing_style = Style.null()
189 self._background_style = Style.null()
190 self._style_cache: Dict[TokenType, Style] = {}
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 hierarchy
198 # We need to go from most to least specific
199 # e.g. ("foo", "bar", "baz") to ("foo", "bar") to ("foo",)
200 get_style = self.style_map.get
201 token = tuple(token_type)
202 style = self._missing_style
203 while token:
204 _style = get_style(token)
205 if _style is not None:
206 style = _style
207 break
208 token = token[:-1]
209 self._style_cache[token_type] = style
210 return style
212 def get_background_style(self) -> Style:
213 return self._background_style
216SyntaxPosition = Tuple[int, int]
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 number
223 (starting from 1) and the second integer is the column index (starting from 0).
224 """
226 style: StyleType
227 start: SyntaxPosition
228 end: SyntaxPosition
229 style_before: bool = False
232class PaddingProperty:
233 """Descriptor to get and set padding."""
235 def __get__(self, obj: Syntax, objtype: Type[Syntax]) -> Tuple[int, int, int, int]:
236 """Space around the Syntax."""
237 return obj._padding
239 def __set__(self, obj: Syntax, padding: PaddingDimensions) -> None:
240 obj._padding = Padding.unpack(padding)
243class Syntax(JupyterMixin):
244 """Construct a Syntax object to render syntax highlighted code.
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 """
264 _pygments_style_class: Type[PygmentsStyle]
265 _theme: SyntaxTheme
267 @classmethod
268 def get_theme(cls, name: Union[str, SyntaxTheme]) -> SyntaxTheme:
269 """Get a syntax theme instance."""
270 if isinstance(name, SyntaxTheme):
271 return name
272 theme: SyntaxTheme
273 if name in RICH_SYNTAX_THEMES:
274 theme = ANSISyntaxTheme(RICH_SYNTAX_THEMES[name])
275 else:
276 theme = PygmentsSyntaxTheme(name)
277 return theme
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 = code
298 self._lexer = lexer
299 self.dedent = dedent
300 self.line_numbers = line_numbers
301 self.start_line = start_line
302 self.line_range = line_range
303 self.highlight_lines = highlight_lines or set()
304 self.code_width = code_width
305 self.tab_size = tab_size
306 self.word_wrap = word_wrap
307 self.background_color = background_color
308 self.background_style = (
309 Style(bgcolor=background_color) if background_color else Style()
310 )
311 self.indent_guides = indent_guides
312 self._padding = Padding.unpack(padding)
314 self._theme = self.get_theme(theme)
315 self._stylized_ranges: List[_SyntaxHighlightRange] = []
317 padding = PaddingProperty()
319 @classmethod
320 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.
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).
357 Returns:
358 [Syntax]: A Syntax object that may be printed to the console
359 """
360 code = Path(path).read_text(encoding=encoding)
362 if not lexer:
363 lexer = cls.guess_lexer(path, code=code)
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 )
382 @classmethod
383 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 the
386 best lexer to use. For example, if the file is ``index.html`` and the file contains Django
387 templating syntax, then "html+django" will be returned. If the file is ``index.html``, and no
388 templating language is used, the "html" lexer will be used. If no string of code
389 is supplied, the lexer will be chosen based on the file extension..
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 lexer
394 is found for the supplied path.
396 Returns:
397 str: The name of the Pygments lexer that best matches the supplied path/code.
398 """
399 lexer: Optional[Lexer] = None
400 lexer_name = "default"
401 if code:
402 try:
403 lexer = guess_lexer_for_filename(path, code)
404 except ClassNotFound:
405 pass
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 pass
416 if lexer:
417 if lexer.aliases:
418 lexer_name = lexer.aliases[0]
419 else:
420 lexer_name = lexer.name
422 return lexer_name
424 def _get_base_style(self) -> Style:
425 """Get the base style."""
426 default_style = self._theme.get_background_style() + self.background_style
427 return default_style
429 def _get_token_color(self, token_type: TokenType) -> Optional[Color]:
430 """Get a color (if any) for the given token.
432 Args:
433 token_type (TokenType): A token type tuple from Pygments.
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.color
441 @property
442 def lexer(self) -> Optional[Lexer]:
443 """The lexer for this syntax, or None if no lexer was found.
445 Tries to find the lexer by name if a string was passed to the constructor.
446 """
448 if isinstance(self._lexer, Lexer):
449 return self._lexer
450 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 None
460 @property
461 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 )
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.
477 Args:
478 code (str): Code to highlight.
479 line_range(Tuple[int, int], optional): Optional line range to highlight.
481 Returns:
482 Text: A text instance containing highlighted syntax.
483 """
485 base_style = self._get_base_style()
486 justify: JustifyMethod = (
487 "default" if base_style.transparent_background else "left"
488 )
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_token
498 lexer = self.lexer or self.default_lexer
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 code
505 # This speeds up further operations as there are less spans to process
506 line_start, line_end = line_range
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 point
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_line
517 def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]:
518 """Convert tokens to spans."""
519 tokens = iter(line_tokenize())
520 line_no = 0
521 _line_start = line_start - 1 if line_start else 0
523 # Skip over tokens until line start
524 while line_no < _line_start:
525 try:
526 _token_type, token = next(tokens)
527 except StopIteration:
528 break
529 yield (token, None)
530 if token.endswith("\n"):
531 line_no += 1
532 # Generate spans until line end
533 for token_type, token in tokens:
534 yield (token, _get_theme_style(token_type))
535 if token.endswith("\n"):
536 line_no += 1
537 if line_end and line_no >= line_end:
538 break
540 text.append_tokens(tokens_to_spans())
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}")
550 if self._stylized_ranges:
551 self._apply_stylized_ranges(text)
553 return text
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.
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 )
576 def _get_line_numbers_color(self, blend: float = 0.3) -> Color:
577 background_style = self._theme.get_background_style() + self.background_style
578 background_color = background_style.bgcolor
579 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)
591 @property
592 def _numbers_column_width(self) -> int:
593 """Get the number of characters used to render the numbers column."""
594 column_width = 0
595 if self.line_numbers:
596 column_width = (
597 len(str(self.start_line + self.code.count("\n")))
598 + NUMBERS_COLUMN_DEFAULT_PADDING
599 )
600 return column_width
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_style
625 def __rich_measure__(
626 self, console: "Console", options: "ConsoleOptions"
627 ) -> "Measurement":
628 _, right, _, left = self.padding
629 padding = left + right
630 if self.code_width is not None:
631 width = self.code_width + self._numbers_column_width + padding + 1
632 return Measurement(self._numbers_column_width, width)
633 lines = self.code.splitlines()
634 width = (
635 self._numbers_column_width
636 + padding
637 + (max(cell_len(line) for line in lines) if lines else 0)
638 )
639 if self.line_numbers:
640 width += 1
641 return Measurement(self._numbers_column_width, width)
643 def __rich_console__(
644 self, console: Console, options: ConsoleOptions
645 ) -> 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 segments
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 padding
659 """
660 transparent_background = self._get_base_style().transparent_background
661 _pad_top, pad_right, _pad_bottom, pad_left = self.padding
662 horizontal_padding = pad_left + pad_right
663 code_width = (
664 (
665 (options.max_width - self._numbers_column_width - 1)
666 if self.line_numbers
667 else options.max_width
668 )
669 - horizontal_padding
670 if self.code_width is None
671 else self.code_width
672 )
673 code_width = max(0, code_width)
675 ends_on_nl, processed_code = self._process_code(self.code)
676 text = self.highlight(processed_code, self.line_range)
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 text
682 style = (
683 self._get_base_style()
684 + self._theme.get_style_for_token(Comment)
685 + Style(dim=True)
686 + self.background_style
687 )
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_line
705 return
707 start_line, end_line = self.line_range or (None, None)
708 line_offset = 0
709 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 return
715 lines = lines[line_offset:end_line]
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_style
723 )
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 )
731 numbers_column_width = self._numbers_column_width
732 render_options = options.update(width=code_width)
734 highlight_line = self.highlight_lines.__contains__
735 _Segment = Segment
736 new_line = _Segment("\n")
738 line_pointer = "> " if options.legacy_windows else "❱ "
740 (
741 background_style,
742 number_style,
743 highlight_number_style,
744 ) = self._get_number_styles(console)
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 ]
768 if self.line_numbers:
769 wrapped_line_left_pad = _Segment(
770 " " * numbers_column_width + " ", background_style
771 )
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_pad
783 yield from wrapped_line
784 yield new_line
785 else:
786 for wrapped_line in wrapped_lines:
787 yield from wrapped_line
788 yield new_line
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.
795 Args:
796 text (Text): Text instance to apply the style to.
797 """
798 code = text.plain
799 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() + 1
805 for match in re.finditer("\n", code, flags=re.MULTILINE)
806 ],
807 len(code) + 1,
808 ]
810 for stylized_range in self._stylized_ranges:
811 start = _get_code_index_for_syntax_position(
812 newlines_offsets, stylized_range.start
813 )
814 end = _get_code_index_for_syntax_position(
815 newlines_offsets, stylized_range.end
816 )
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)
823 def _process_code(self, code: str) -> Tuple[bool, str]:
824 """
825 Applies various processing to a raw code string
826 (normalises it so it always ends with a line return, dedents it if necessary, etc.)
828 Args:
829 code (str): The raw code string to process
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_code
839 )
840 processed_code = processed_code.expandtabs(self.tab_size)
841 return ends_on_nl, processed_code
844def _get_code_index_for_syntax_position(
845 newlines_offsets: Sequence[int], position: SyntaxPosition
846) -> Optional[int]:
847 """
848 Returns the index of the code string for the given positions.
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.
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 range
857 we silently clamp its value so that it reaches the end of the line)
858 """
859 lines_count = len(newlines_offsets)
861 line_number, column_index = position
862 if line_number > lines_count or len(newlines_offsets) < (line_number + 1):
863 return None # `line_number` is out of range
864 line_index = line_number - 1
865 line_length = newlines_offsets[line_index + 1] - newlines_offsets[line_index] - 1
866 # 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_index
871if __name__ == "__main__": # pragma: no cover
872 import argparse
873 import sys
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()
959 from rich.console import Console
961 console = Console(force_terminal=args.force_color, width=args.width)
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)