Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rich/syntax.py: 30%

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

334 statements  

1import os.path 

2import re 

3import sys 

4import textwrap 

5from abc import ABC, abstractmethod 

6from pathlib import Path 

7from typing import ( 

8 Any, 

9 Dict, 

10 Iterable, 

11 List, 

12 NamedTuple, 

13 Optional, 

14 Sequence, 

15 Set, 

16 Tuple, 

17 Type, 

18 Union, 

19) 

20 

21from pygments.lexer import Lexer 

22from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename 

23from pygments.style import Style as PygmentsStyle 

24from pygments.styles import get_style_by_name 

25from pygments.token import ( 

26 Comment, 

27 Error, 

28 Generic, 

29 Keyword, 

30 Name, 

31 Number, 

32 Operator, 

33 String, 

34 Token, 

35 Whitespace, 

36) 

37from pygments.util import ClassNotFound 

38 

39from rich.containers import Lines 

40from rich.padding import Padding, PaddingDimensions 

41 

42from ._loop import loop_first 

43from .cells import cell_len 

44from .color import Color, blend_rgb 

45from .console import Console, ConsoleOptions, JustifyMethod, RenderResult 

46from .jupyter import JupyterMixin 

47from .measure import Measurement 

48from .segment import Segment, Segments 

49from .style import Style, StyleType 

50from .text import Text 

51 

52TokenType = Tuple[str, ...] 

53 

54WINDOWS = sys.platform == "win32" 

55DEFAULT_THEME = "monokai" 

56 

57# The following styles are based on https://github.com/pygments/pygments/blob/master/pygments/formatters/terminal.py 

58# A few modifications were made 

59 

60ANSI_LIGHT: Dict[TokenType, Style] = { 

61 Token: Style(), 

62 Whitespace: Style(color="white"), 

63 Comment: Style(dim=True), 

64 Comment.Preproc: Style(color="cyan"), 

65 Keyword: Style(color="blue"), 

66 Keyword.Type: Style(color="cyan"), 

67 Operator.Word: Style(color="magenta"), 

68 Name.Builtin: Style(color="cyan"), 

69 Name.Function: Style(color="green"), 

70 Name.Namespace: Style(color="cyan", underline=True), 

71 Name.Class: Style(color="green", underline=True), 

72 Name.Exception: Style(color="cyan"), 

73 Name.Decorator: Style(color="magenta", bold=True), 

74 Name.Variable: Style(color="red"), 

75 Name.Constant: Style(color="red"), 

76 Name.Attribute: Style(color="cyan"), 

77 Name.Tag: Style(color="bright_blue"), 

78 String: Style(color="yellow"), 

79 Number: Style(color="blue"), 

80 Generic.Deleted: Style(color="bright_red"), 

81 Generic.Inserted: Style(color="green"), 

82 Generic.Heading: Style(bold=True), 

83 Generic.Subheading: Style(color="magenta", bold=True), 

84 Generic.Prompt: Style(bold=True), 

85 Generic.Error: Style(color="bright_red"), 

86 Error: Style(color="red", underline=True), 

87} 

88 

89ANSI_DARK: Dict[TokenType, Style] = { 

90 Token: Style(), 

91 Whitespace: Style(color="bright_black"), 

92 Comment: Style(dim=True), 

93 Comment.Preproc: Style(color="bright_cyan"), 

94 Keyword: Style(color="bright_blue"), 

95 Keyword.Type: Style(color="bright_cyan"), 

96 Operator.Word: Style(color="bright_magenta"), 

97 Name.Builtin: Style(color="bright_cyan"), 

98 Name.Function: Style(color="bright_green"), 

99 Name.Namespace: Style(color="bright_cyan", underline=True), 

100 Name.Class: Style(color="bright_green", underline=True), 

101 Name.Exception: Style(color="bright_cyan"), 

102 Name.Decorator: Style(color="bright_magenta", bold=True), 

103 Name.Variable: Style(color="bright_red"), 

104 Name.Constant: Style(color="bright_red"), 

105 Name.Attribute: Style(color="bright_cyan"), 

106 Name.Tag: Style(color="bright_blue"), 

107 String: Style(color="yellow"), 

108 Number: Style(color="bright_blue"), 

109 Generic.Deleted: Style(color="bright_red"), 

110 Generic.Inserted: Style(color="bright_green"), 

111 Generic.Heading: Style(bold=True), 

112 Generic.Subheading: Style(color="bright_magenta", bold=True), 

113 Generic.Prompt: Style(bold=True), 

114 Generic.Error: Style(color="bright_red"), 

115 Error: Style(color="red", underline=True), 

116} 

117 

118RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK} 

119NUMBERS_COLUMN_DEFAULT_PADDING = 2 

120 

121 

122class SyntaxTheme(ABC): 

123 """Base class for a syntax theme.""" 

124 

125 @abstractmethod 

126 def get_style_for_token(self, token_type: TokenType) -> Style: 

127 """Get a style for a given Pygments token.""" 

128 raise NotImplementedError # pragma: no cover 

129 

130 @abstractmethod 

131 def get_background_style(self) -> Style: 

132 """Get the background color.""" 

133 raise NotImplementedError # pragma: no cover 

134 

135 

136class PygmentsSyntaxTheme(SyntaxTheme): 

137 """Syntax theme that delegates to Pygments theme.""" 

138 

139 def __init__(self, theme: Union[str, Type[PygmentsStyle]]) -> None: 

140 self._style_cache: Dict[TokenType, Style] = {} 

141 if isinstance(theme, str): 

142 try: 

143 self._pygments_style_class = get_style_by_name(theme) 

144 except ClassNotFound: 

145 self._pygments_style_class = get_style_by_name("default") 

146 else: 

147 self._pygments_style_class = theme 

148 

149 self._background_color = self._pygments_style_class.background_color 

150 self._background_style = Style(bgcolor=self._background_color) 

151 

152 def get_style_for_token(self, token_type: TokenType) -> Style: 

153 """Get a style from a Pygments class.""" 

154 try: 

155 return self._style_cache[token_type] 

156 except KeyError: 

157 try: 

158 pygments_style = self._pygments_style_class.style_for_token(token_type) 

159 except KeyError: 

160 style = Style.null() 

161 else: 

162 color = pygments_style["color"] 

163 bgcolor = pygments_style["bgcolor"] 

164 style = Style( 

165 color="#" + color if color else "#000000", 

166 bgcolor="#" + bgcolor if bgcolor else self._background_color, 

167 bold=pygments_style["bold"], 

168 italic=pygments_style["italic"], 

169 underline=pygments_style["underline"], 

170 ) 

171 self._style_cache[token_type] = style 

172 return style 

173 

174 def get_background_style(self) -> Style: 

175 return self._background_style 

176 

177 

178class ANSISyntaxTheme(SyntaxTheme): 

179 """Syntax theme to use standard colors.""" 

180 

181 def __init__(self, style_map: Dict[TokenType, Style]) -> None: 

182 self.style_map = style_map 

183 self._missing_style = Style.null() 

184 self._background_style = Style.null() 

185 self._style_cache: Dict[TokenType, Style] = {} 

186 

187 def get_style_for_token(self, token_type: TokenType) -> Style: 

188 """Look up style in the style map.""" 

189 try: 

190 return self._style_cache[token_type] 

191 except KeyError: 

192 # Styles form a hierarchy 

193 # We need to go from most to least specific 

194 # e.g. ("foo", "bar", "baz") to ("foo", "bar") to ("foo",) 

195 get_style = self.style_map.get 

196 token = tuple(token_type) 

197 style = self._missing_style 

198 while token: 

199 _style = get_style(token) 

200 if _style is not None: 

201 style = _style 

202 break 

203 token = token[:-1] 

204 self._style_cache[token_type] = style 

205 return style 

206 

207 def get_background_style(self) -> Style: 

208 return self._background_style 

209 

210 

211SyntaxPosition = Tuple[int, int] 

212 

213 

214class _SyntaxHighlightRange(NamedTuple): 

215 """ 

216 A range to highlight in a Syntax object. 

217 `start` and `end` are 2-integers tuples, where the first integer is the line number 

218 (starting from 1) and the second integer is the column index (starting from 0). 

219 """ 

220 

221 style: StyleType 

222 start: SyntaxPosition 

223 end: SyntaxPosition 

224 style_before: bool = False 

225 

226 

227class Syntax(JupyterMixin): 

228 """Construct a Syntax object to render syntax highlighted code. 

229 

230 Args: 

231 code (str): Code to highlight. 

232 lexer (Lexer | str): Lexer to use (see https://pygments.org/docs/lexers/) 

233 theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "monokai". 

234 dedent (bool, optional): Enable stripping of initial whitespace. Defaults to False. 

235 line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False. 

236 start_line (int, optional): Starting number for line numbers. Defaults to 1. 

237 line_range (Tuple[int | None, int | None], optional): If given should be a tuple of the start and end line to render. 

238 A value of None in the tuple indicates the range is open in that direction. 

239 highlight_lines (Set[int]): A set of line numbers to highlight. 

240 code_width: Width of code to render (not including line numbers), or ``None`` to use all available width. 

241 tab_size (int, optional): Size of tabs. Defaults to 4. 

242 word_wrap (bool, optional): Enable word wrapping. 

243 background_color (str, optional): Optional background color, or None to use theme color. Defaults to None. 

244 indent_guides (bool, optional): Show indent guides. Defaults to False. 

245 padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding). 

246 """ 

247 

248 _pygments_style_class: Type[PygmentsStyle] 

249 _theme: SyntaxTheme 

250 

251 @classmethod 

252 def get_theme(cls, name: Union[str, SyntaxTheme]) -> SyntaxTheme: 

253 """Get a syntax theme instance.""" 

254 if isinstance(name, SyntaxTheme): 

255 return name 

256 theme: SyntaxTheme 

257 if name in RICH_SYNTAX_THEMES: 

258 theme = ANSISyntaxTheme(RICH_SYNTAX_THEMES[name]) 

259 else: 

260 theme = PygmentsSyntaxTheme(name) 

261 return theme 

262 

263 def __init__( 

264 self, 

265 code: str, 

266 lexer: Union[Lexer, str], 

267 *, 

268 theme: Union[str, SyntaxTheme] = DEFAULT_THEME, 

269 dedent: bool = False, 

270 line_numbers: bool = False, 

271 start_line: int = 1, 

272 line_range: Optional[Tuple[Optional[int], Optional[int]]] = None, 

273 highlight_lines: Optional[Set[int]] = None, 

274 code_width: Optional[int] = None, 

275 tab_size: int = 4, 

276 word_wrap: bool = False, 

277 background_color: Optional[str] = None, 

278 indent_guides: bool = False, 

279 padding: PaddingDimensions = 0, 

280 ) -> None: 

281 self.code = code 

282 self._lexer = lexer 

283 self.dedent = dedent 

284 self.line_numbers = line_numbers 

285 self.start_line = start_line 

286 self.line_range = line_range 

287 self.highlight_lines = highlight_lines or set() 

288 self.code_width = code_width 

289 self.tab_size = tab_size 

290 self.word_wrap = word_wrap 

291 self.background_color = background_color 

292 self.background_style = ( 

293 Style(bgcolor=background_color) if background_color else Style() 

294 ) 

295 self.indent_guides = indent_guides 

296 self.padding = padding 

297 

298 self._theme = self.get_theme(theme) 

299 self._stylized_ranges: List[_SyntaxHighlightRange] = [] 

300 

301 @classmethod 

302 def from_path( 

303 cls, 

304 path: str, 

305 encoding: str = "utf-8", 

306 lexer: Optional[Union[Lexer, str]] = None, 

307 theme: Union[str, SyntaxTheme] = DEFAULT_THEME, 

308 dedent: bool = False, 

309 line_numbers: bool = False, 

310 line_range: Optional[Tuple[int, int]] = None, 

311 start_line: int = 1, 

312 highlight_lines: Optional[Set[int]] = None, 

313 code_width: Optional[int] = None, 

314 tab_size: int = 4, 

315 word_wrap: bool = False, 

316 background_color: Optional[str] = None, 

317 indent_guides: bool = False, 

318 padding: PaddingDimensions = 0, 

319 ) -> "Syntax": 

320 """Construct a Syntax object from a file. 

321 

322 Args: 

323 path (str): Path to file to highlight. 

324 encoding (str): Encoding of file. 

325 lexer (str | Lexer, optional): Lexer to use. If None, lexer will be auto-detected from path/file content. 

326 theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "emacs". 

327 dedent (bool, optional): Enable stripping of initial whitespace. Defaults to True. 

328 line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False. 

329 start_line (int, optional): Starting number for line numbers. Defaults to 1. 

330 line_range (Tuple[int, int], optional): If given should be a tuple of the start and end line to render. 

331 highlight_lines (Set[int]): A set of line numbers to highlight. 

332 code_width: Width of code to render (not including line numbers), or ``None`` to use all available width. 

333 tab_size (int, optional): Size of tabs. Defaults to 4. 

334 word_wrap (bool, optional): Enable word wrapping of code. 

335 background_color (str, optional): Optional background color, or None to use theme color. Defaults to None. 

336 indent_guides (bool, optional): Show indent guides. Defaults to False. 

337 padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding). 

338 

339 Returns: 

340 [Syntax]: A Syntax object that may be printed to the console 

341 """ 

342 code = Path(path).read_text(encoding=encoding) 

343 

344 if not lexer: 

345 lexer = cls.guess_lexer(path, code=code) 

346 

347 return cls( 

348 code, 

349 lexer, 

350 theme=theme, 

351 dedent=dedent, 

352 line_numbers=line_numbers, 

353 line_range=line_range, 

354 start_line=start_line, 

355 highlight_lines=highlight_lines, 

356 code_width=code_width, 

357 tab_size=tab_size, 

358 word_wrap=word_wrap, 

359 background_color=background_color, 

360 indent_guides=indent_guides, 

361 padding=padding, 

362 ) 

363 

364 @classmethod 

365 def guess_lexer(cls, path: str, code: Optional[str] = None) -> str: 

366 """Guess the alias of the Pygments lexer to use based on a path and an optional string of code. 

367 If code is supplied, it will use a combination of the code and the filename to determine the 

368 best lexer to use. For example, if the file is ``index.html`` and the file contains Django 

369 templating syntax, then "html+django" will be returned. If the file is ``index.html``, and no 

370 templating language is used, the "html" lexer will be used. If no string of code 

371 is supplied, the lexer will be chosen based on the file extension.. 

372 

373 Args: 

374 path (AnyStr): The path to the file containing the code you wish to know the lexer for. 

375 code (str, optional): Optional string of code that will be used as a fallback if no lexer 

376 is found for the supplied path. 

377 

378 Returns: 

379 str: The name of the Pygments lexer that best matches the supplied path/code. 

380 """ 

381 lexer: Optional[Lexer] = None 

382 lexer_name = "default" 

383 if code: 

384 try: 

385 lexer = guess_lexer_for_filename(path, code) 

386 except ClassNotFound: 

387 pass 

388 

389 if not lexer: 

390 try: 

391 _, ext = os.path.splitext(path) 

392 if ext: 

393 extension = ext.lstrip(".").lower() 

394 lexer = get_lexer_by_name(extension) 

395 except ClassNotFound: 

396 pass 

397 

398 if lexer: 

399 if lexer.aliases: 

400 lexer_name = lexer.aliases[0] 

401 else: 

402 lexer_name = lexer.name 

403 

404 return lexer_name 

405 

406 def _get_base_style(self) -> Style: 

407 """Get the base style.""" 

408 default_style = self._theme.get_background_style() + self.background_style 

409 return default_style 

410 

411 def _get_token_color(self, token_type: TokenType) -> Optional[Color]: 

412 """Get a color (if any) for the given token. 

413 

414 Args: 

415 token_type (TokenType): A token type tuple from Pygments. 

416 

417 Returns: 

418 Optional[Color]: Color from theme, or None for no color. 

419 """ 

420 style = self._theme.get_style_for_token(token_type) 

421 return style.color 

422 

423 @property 

424 def lexer(self) -> Optional[Lexer]: 

425 """The lexer for this syntax, or None if no lexer was found. 

426 

427 Tries to find the lexer by name if a string was passed to the constructor. 

428 """ 

429 

430 if isinstance(self._lexer, Lexer): 

431 return self._lexer 

432 try: 

433 return get_lexer_by_name( 

434 self._lexer, 

435 stripnl=False, 

436 ensurenl=True, 

437 tabsize=self.tab_size, 

438 ) 

439 except ClassNotFound: 

440 return None 

441 

442 @property 

443 def default_lexer(self) -> Lexer: 

444 """A Pygments Lexer to use if one is not specified or invalid.""" 

445 return get_lexer_by_name( 

446 "text", 

447 stripnl=False, 

448 ensurenl=True, 

449 tabsize=self.tab_size, 

450 ) 

451 

452 def highlight( 

453 self, 

454 code: str, 

455 line_range: Optional[Tuple[Optional[int], Optional[int]]] = None, 

456 ) -> Text: 

457 """Highlight code and return a Text instance. 

458 

459 Args: 

460 code (str): Code to highlight. 

461 line_range(Tuple[int, int], optional): Optional line range to highlight. 

462 

463 Returns: 

464 Text: A text instance containing highlighted syntax. 

465 """ 

466 

467 base_style = self._get_base_style() 

468 justify: JustifyMethod = ( 

469 "default" if base_style.transparent_background else "left" 

470 ) 

471 

472 text = Text( 

473 justify=justify, 

474 style=base_style, 

475 tab_size=self.tab_size, 

476 no_wrap=not self.word_wrap, 

477 ) 

478 _get_theme_style = self._theme.get_style_for_token 

479 

480 lexer = self.lexer or self.default_lexer 

481 

482 if lexer is None: 

483 text.append(code) 

484 else: 

485 if line_range: 

486 # More complicated path to only stylize a portion of the code 

487 # This speeds up further operations as there are less spans to process 

488 line_start, line_end = line_range 

489 

490 def line_tokenize() -> Iterable[Tuple[Any, str]]: 

491 """Split tokens to one per line.""" 

492 assert lexer # required to make MyPy happy - we know lexer is not None at this point 

493 

494 for token_type, token in lexer.get_tokens(code): 

495 while token: 

496 line_token, new_line, token = token.partition("\n") 

497 yield token_type, line_token + new_line 

498 

499 def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]: 

500 """Convert tokens to spans.""" 

501 tokens = iter(line_tokenize()) 

502 line_no = 0 

503 _line_start = line_start - 1 if line_start else 0 

504 

505 # Skip over tokens until line start 

506 while line_no < _line_start: 

507 try: 

508 _token_type, token = next(tokens) 

509 except StopIteration: 

510 break 

511 yield (token, None) 

512 if token.endswith("\n"): 

513 line_no += 1 

514 # Generate spans until line end 

515 for token_type, token in tokens: 

516 yield (token, _get_theme_style(token_type)) 

517 if token.endswith("\n"): 

518 line_no += 1 

519 if line_end and line_no >= line_end: 

520 break 

521 

522 text.append_tokens(tokens_to_spans()) 

523 

524 else: 

525 text.append_tokens( 

526 (token, _get_theme_style(token_type)) 

527 for token_type, token in lexer.get_tokens(code) 

528 ) 

529 if self.background_color is not None: 

530 text.stylize(f"on {self.background_color}") 

531 

532 if self._stylized_ranges: 

533 self._apply_stylized_ranges(text) 

534 

535 return text 

536 

537 def stylize_range( 

538 self, 

539 style: StyleType, 

540 start: SyntaxPosition, 

541 end: SyntaxPosition, 

542 style_before: bool = False, 

543 ) -> None: 

544 """ 

545 Adds a custom style on a part of the code, that will be applied to the syntax display when it's rendered. 

546 Line numbers are 1-based, while column indexes are 0-based. 

547 

548 Args: 

549 style (StyleType): The style to apply. 

550 start (Tuple[int, int]): The start of the range, in the form `[line number, column index]`. 

551 end (Tuple[int, int]): The end of the range, in the form `[line number, column index]`. 

552 style_before (bool): Apply the style before any existing styles. 

553 """ 

554 self._stylized_ranges.append( 

555 _SyntaxHighlightRange(style, start, end, style_before) 

556 ) 

557 

558 def _get_line_numbers_color(self, blend: float = 0.3) -> Color: 

559 background_style = self._theme.get_background_style() + self.background_style 

560 background_color = background_style.bgcolor 

561 if background_color is None or background_color.is_system_defined: 

562 return Color.default() 

563 foreground_color = self._get_token_color(Token.Text) 

564 if foreground_color is None or foreground_color.is_system_defined: 

565 return foreground_color or Color.default() 

566 new_color = blend_rgb( 

567 background_color.get_truecolor(), 

568 foreground_color.get_truecolor(), 

569 cross_fade=blend, 

570 ) 

571 return Color.from_triplet(new_color) 

572 

573 @property 

574 def _numbers_column_width(self) -> int: 

575 """Get the number of characters used to render the numbers column.""" 

576 column_width = 0 

577 if self.line_numbers: 

578 column_width = ( 

579 len(str(self.start_line + self.code.count("\n"))) 

580 + NUMBERS_COLUMN_DEFAULT_PADDING 

581 ) 

582 return column_width 

583 

584 def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]: 

585 """Get background, number, and highlight styles for line numbers.""" 

586 background_style = self._get_base_style() 

587 if background_style.transparent_background: 

588 return Style.null(), Style(dim=True), Style.null() 

589 if console.color_system in ("256", "truecolor"): 

590 number_style = Style.chain( 

591 background_style, 

592 self._theme.get_style_for_token(Token.Text), 

593 Style(color=self._get_line_numbers_color()), 

594 self.background_style, 

595 ) 

596 highlight_number_style = Style.chain( 

597 background_style, 

598 self._theme.get_style_for_token(Token.Text), 

599 Style(bold=True, color=self._get_line_numbers_color(0.9)), 

600 self.background_style, 

601 ) 

602 else: 

603 number_style = background_style + Style(dim=True) 

604 highlight_number_style = background_style + Style(dim=False) 

605 return background_style, number_style, highlight_number_style 

606 

607 def __rich_measure__( 

608 self, console: "Console", options: "ConsoleOptions" 

609 ) -> "Measurement": 

610 _, right, _, left = Padding.unpack(self.padding) 

611 padding = left + right 

612 if self.code_width is not None: 

613 width = self.code_width + self._numbers_column_width + padding + 1 

614 return Measurement(self._numbers_column_width, width) 

615 lines = self.code.splitlines() 

616 width = ( 

617 self._numbers_column_width 

618 + padding 

619 + (max(cell_len(line) for line in lines) if lines else 0) 

620 ) 

621 if self.line_numbers: 

622 width += 1 

623 return Measurement(self._numbers_column_width, width) 

624 

625 def __rich_console__( 

626 self, console: Console, options: ConsoleOptions 

627 ) -> RenderResult: 

628 segments = Segments(self._get_syntax(console, options)) 

629 if self.padding: 

630 yield Padding(segments, style=self._get_base_style(), pad=self.padding) 

631 else: 

632 yield segments 

633 

634 def _get_syntax( 

635 self, 

636 console: Console, 

637 options: ConsoleOptions, 

638 ) -> Iterable[Segment]: 

639 """ 

640 Get the Segments for the Syntax object, excluding any vertical/horizontal padding 

641 """ 

642 transparent_background = self._get_base_style().transparent_background 

643 code_width = ( 

644 ( 

645 (options.max_width - self._numbers_column_width - 1) 

646 if self.line_numbers 

647 else options.max_width 

648 ) 

649 if self.code_width is None 

650 else self.code_width 

651 ) 

652 

653 ends_on_nl, processed_code = self._process_code(self.code) 

654 text = self.highlight(processed_code, self.line_range) 

655 

656 if not self.line_numbers and not self.word_wrap and not self.line_range: 

657 if not ends_on_nl: 

658 text.remove_suffix("\n") 

659 # Simple case of just rendering text 

660 style = ( 

661 self._get_base_style() 

662 + self._theme.get_style_for_token(Comment) 

663 + Style(dim=True) 

664 + self.background_style 

665 ) 

666 if self.indent_guides and not options.ascii_only: 

667 text = text.with_indent_guides(self.tab_size, style=style) 

668 text.overflow = "crop" 

669 if style.transparent_background: 

670 yield from console.render( 

671 text, options=options.update(width=code_width) 

672 ) 

673 else: 

674 syntax_lines = console.render_lines( 

675 text, 

676 options.update(width=code_width, height=None, justify="left"), 

677 style=self.background_style, 

678 pad=True, 

679 new_lines=True, 

680 ) 

681 for syntax_line in syntax_lines: 

682 yield from syntax_line 

683 return 

684 

685 start_line, end_line = self.line_range or (None, None) 

686 line_offset = 0 

687 if start_line: 

688 line_offset = max(0, start_line - 1) 

689 lines: Union[List[Text], Lines] = text.split("\n", allow_blank=ends_on_nl) 

690 if self.line_range: 

691 if line_offset > len(lines): 

692 return 

693 lines = lines[line_offset:end_line] 

694 

695 if self.indent_guides and not options.ascii_only: 

696 style = ( 

697 self._get_base_style() 

698 + self._theme.get_style_for_token(Comment) 

699 + Style(dim=True) 

700 + self.background_style 

701 ) 

702 lines = ( 

703 Text("\n") 

704 .join(lines) 

705 .with_indent_guides(self.tab_size, style=style + Style(italic=False)) 

706 .split("\n", allow_blank=True) 

707 ) 

708 

709 numbers_column_width = self._numbers_column_width 

710 render_options = options.update(width=code_width) 

711 

712 highlight_line = self.highlight_lines.__contains__ 

713 _Segment = Segment 

714 new_line = _Segment("\n") 

715 

716 line_pointer = "> " if options.legacy_windows else "❱ " 

717 

718 ( 

719 background_style, 

720 number_style, 

721 highlight_number_style, 

722 ) = self._get_number_styles(console) 

723 

724 for line_no, line in enumerate(lines, self.start_line + line_offset): 

725 if self.word_wrap: 

726 wrapped_lines = console.render_lines( 

727 line, 

728 render_options.update(height=None, justify="left"), 

729 style=background_style, 

730 pad=not transparent_background, 

731 ) 

732 else: 

733 segments = list(line.render(console, end="")) 

734 if options.no_wrap: 

735 wrapped_lines = [segments] 

736 else: 

737 wrapped_lines = [ 

738 _Segment.adjust_line_length( 

739 segments, 

740 render_options.max_width, 

741 style=background_style, 

742 pad=not transparent_background, 

743 ) 

744 ] 

745 

746 if self.line_numbers: 

747 wrapped_line_left_pad = _Segment( 

748 " " * numbers_column_width + " ", background_style 

749 ) 

750 for first, wrapped_line in loop_first(wrapped_lines): 

751 if first: 

752 line_column = str(line_no).rjust(numbers_column_width - 2) + " " 

753 if highlight_line(line_no): 

754 yield _Segment(line_pointer, Style(color="red")) 

755 yield _Segment(line_column, highlight_number_style) 

756 else: 

757 yield _Segment(" ", highlight_number_style) 

758 yield _Segment(line_column, number_style) 

759 else: 

760 yield wrapped_line_left_pad 

761 yield from wrapped_line 

762 yield new_line 

763 else: 

764 for wrapped_line in wrapped_lines: 

765 yield from wrapped_line 

766 yield new_line 

767 

768 def _apply_stylized_ranges(self, text: Text) -> None: 

769 """ 

770 Apply stylized ranges to a text instance, 

771 using the given code to determine the right portion to apply the style to. 

772 

773 Args: 

774 text (Text): Text instance to apply the style to. 

775 """ 

776 code = text.plain 

777 newlines_offsets = [ 

778 # Let's add outer boundaries at each side of the list: 

779 0, 

780 # N.B. using "\n" here is much faster than using metacharacters such as "^" or "\Z": 

781 *[ 

782 match.start() + 1 

783 for match in re.finditer("\n", code, flags=re.MULTILINE) 

784 ], 

785 len(code) + 1, 

786 ] 

787 

788 for stylized_range in self._stylized_ranges: 

789 start = _get_code_index_for_syntax_position( 

790 newlines_offsets, stylized_range.start 

791 ) 

792 end = _get_code_index_for_syntax_position( 

793 newlines_offsets, stylized_range.end 

794 ) 

795 if start is not None and end is not None: 

796 if stylized_range.style_before: 

797 text.stylize_before(stylized_range.style, start, end) 

798 else: 

799 text.stylize(stylized_range.style, start, end) 

800 

801 def _process_code(self, code: str) -> Tuple[bool, str]: 

802 """ 

803 Applies various processing to a raw code string 

804 (normalises it so it always ends with a line return, dedents it if necessary, etc.) 

805 

806 Args: 

807 code (str): The raw code string to process 

808 

809 Returns: 

810 Tuple[bool, str]: the boolean indicates whether the raw code ends with a line return, 

811 while the string is the processed code. 

812 """ 

813 ends_on_nl = code.endswith("\n") 

814 processed_code = code if ends_on_nl else code + "\n" 

815 processed_code = ( 

816 textwrap.dedent(processed_code) if self.dedent else processed_code 

817 ) 

818 processed_code = processed_code.expandtabs(self.tab_size) 

819 return ends_on_nl, processed_code 

820 

821 

822def _get_code_index_for_syntax_position( 

823 newlines_offsets: Sequence[int], position: SyntaxPosition 

824) -> Optional[int]: 

825 """ 

826 Returns the index of the code string for the given positions. 

827 

828 Args: 

829 newlines_offsets (Sequence[int]): The offset of each newline character found in the code snippet. 

830 position (SyntaxPosition): The position to search for. 

831 

832 Returns: 

833 Optional[int]: The index of the code string for this position, or `None` 

834 if the given position's line number is out of range (if it's the column that is out of range 

835 we silently clamp its value so that it reaches the end of the line) 

836 """ 

837 lines_count = len(newlines_offsets) 

838 

839 line_number, column_index = position 

840 if line_number > lines_count or len(newlines_offsets) < (line_number + 1): 

841 return None # `line_number` is out of range 

842 line_index = line_number - 1 

843 line_length = newlines_offsets[line_index + 1] - newlines_offsets[line_index] - 1 

844 # If `column_index` is out of range: let's silently clamp it: 

845 column_index = min(line_length, column_index) 

846 return newlines_offsets[line_index] + column_index 

847 

848 

849if __name__ == "__main__": # pragma: no cover 

850 import argparse 

851 import sys 

852 

853 parser = argparse.ArgumentParser( 

854 description="Render syntax to the console with Rich" 

855 ) 

856 parser.add_argument( 

857 "path", 

858 metavar="PATH", 

859 help="path to file, or - for stdin", 

860 ) 

861 parser.add_argument( 

862 "-c", 

863 "--force-color", 

864 dest="force_color", 

865 action="store_true", 

866 default=None, 

867 help="force color for non-terminals", 

868 ) 

869 parser.add_argument( 

870 "-i", 

871 "--indent-guides", 

872 dest="indent_guides", 

873 action="store_true", 

874 default=False, 

875 help="display indent guides", 

876 ) 

877 parser.add_argument( 

878 "-l", 

879 "--line-numbers", 

880 dest="line_numbers", 

881 action="store_true", 

882 help="render line numbers", 

883 ) 

884 parser.add_argument( 

885 "-w", 

886 "--width", 

887 type=int, 

888 dest="width", 

889 default=None, 

890 help="width of output (default will auto-detect)", 

891 ) 

892 parser.add_argument( 

893 "-r", 

894 "--wrap", 

895 dest="word_wrap", 

896 action="store_true", 

897 default=False, 

898 help="word wrap long lines", 

899 ) 

900 parser.add_argument( 

901 "-s", 

902 "--soft-wrap", 

903 action="store_true", 

904 dest="soft_wrap", 

905 default=False, 

906 help="enable soft wrapping mode", 

907 ) 

908 parser.add_argument( 

909 "-t", "--theme", dest="theme", default="monokai", help="pygments theme" 

910 ) 

911 parser.add_argument( 

912 "-b", 

913 "--background-color", 

914 dest="background_color", 

915 default=None, 

916 help="Override background color", 

917 ) 

918 parser.add_argument( 

919 "-x", 

920 "--lexer", 

921 default=None, 

922 dest="lexer_name", 

923 help="Lexer name", 

924 ) 

925 parser.add_argument( 

926 "-p", "--padding", type=int, default=0, dest="padding", help="Padding" 

927 ) 

928 parser.add_argument( 

929 "--highlight-line", 

930 type=int, 

931 default=None, 

932 dest="highlight_line", 

933 help="The line number (not index!) to highlight", 

934 ) 

935 args = parser.parse_args() 

936 

937 from rich.console import Console 

938 

939 console = Console(force_terminal=args.force_color, width=args.width) 

940 

941 if args.path == "-": 

942 code = sys.stdin.read() 

943 syntax = Syntax( 

944 code=code, 

945 lexer=args.lexer_name, 

946 line_numbers=args.line_numbers, 

947 word_wrap=args.word_wrap, 

948 theme=args.theme, 

949 background_color=args.background_color, 

950 indent_guides=args.indent_guides, 

951 padding=args.padding, 

952 highlight_lines={args.highlight_line}, 

953 ) 

954 else: 

955 syntax = Syntax.from_path( 

956 args.path, 

957 lexer=args.lexer_name, 

958 line_numbers=args.line_numbers, 

959 word_wrap=args.word_wrap, 

960 theme=args.theme, 

961 background_color=args.background_color, 

962 indent_guides=args.indent_guides, 

963 padding=args.padding, 

964 highlight_lines={args.highlight_line}, 

965 ) 

966 console.print(syntax, soft_wrap=args.soft_wrap)