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

345 statements  

1from __future__ import annotations 

2 

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) 

23 

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 

41 

42if TYPE_CHECKING: 

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

44 

45from rich.containers import Lines 

46from rich.padding import Padding, PaddingDimensions 

47 

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 

56 

57TokenType = Tuple[str, ...] 

58 

59WINDOWS = sys.platform == "win32" 

60DEFAULT_THEME = "monokai" 

61 

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

63# A few modifications were made 

64 

65ANSI_LIGHT: Dict[TokenType, Style] = { 

66 Token: Style(), 

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

68 Comment: Style(dim=True), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

92} 

93 

94ANSI_DARK: Dict[TokenType, Style] = { 

95 Token: Style(), 

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

97 Comment: Style(dim=True), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

121} 

122 

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

124NUMBERS_COLUMN_DEFAULT_PADDING = 2 

125 

126 

127class SyntaxTheme(ABC): 

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

129 

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 

134 

135 @abstractmethod 

136 def get_background_style(self) -> Style: 

137 """Get the background color.""" 

138 raise NotImplementedError # pragma: no cover 

139 

140 

141class PygmentsSyntaxTheme(SyntaxTheme): 

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

143 

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

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

146 if isinstance(theme, str): 

147 try: 

148 self._pygments_style_class = get_style_by_name(theme) 

149 except ClassNotFound: 

150 self._pygments_style_class = get_style_by_name("default") 

151 else: 

152 self._pygments_style_class = theme 

153 

154 self._background_color = self._pygments_style_class.background_color 

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

156 

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

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

159 try: 

160 return self._style_cache[token_type] 

161 except KeyError: 

162 try: 

163 pygments_style = self._pygments_style_class.style_for_token(token_type) 

164 except KeyError: 

165 style = Style.null() 

166 else: 

167 color = pygments_style["color"] 

168 bgcolor = pygments_style["bgcolor"] 

169 style = Style( 

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

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

172 bold=pygments_style["bold"], 

173 italic=pygments_style["italic"], 

174 underline=pygments_style["underline"], 

175 ) 

176 self._style_cache[token_type] = style 

177 return style 

178 

179 def get_background_style(self) -> Style: 

180 return self._background_style 

181 

182 

183class ANSISyntaxTheme(SyntaxTheme): 

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

185 

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

187 self.style_map = style_map 

188 self._missing_style = Style.null() 

189 self._background_style = Style.null() 

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

191 

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

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

194 try: 

195 return self._style_cache[token_type] 

196 except KeyError: 

197 # Styles form a 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 

211 

212 def get_background_style(self) -> Style: 

213 return self._background_style 

214 

215 

216SyntaxPosition = Tuple[int, int] 

217 

218 

219class _SyntaxHighlightRange(NamedTuple): 

220 """ 

221 A range to highlight in a Syntax object. 

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

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

224 """ 

225 

226 style: StyleType 

227 start: SyntaxPosition 

228 end: SyntaxPosition 

229 style_before: bool = False 

230 

231 

232class PaddingProperty: 

233 """Descriptor to get and set padding.""" 

234 

235 def __get__(self, obj: Syntax, objtype: Type[Syntax]) -> Tuple[int, int, int, int]: 

236 """Space around the Syntax.""" 

237 return obj._padding 

238 

239 def __set__(self, obj: Syntax, padding: PaddingDimensions) -> None: 

240 obj._padding = Padding.unpack(padding) 

241 

242 

243class Syntax(JupyterMixin): 

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

245 

246 Args: 

247 code (str): Code to highlight. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

262 """ 

263 

264 _pygments_style_class: Type[PygmentsStyle] 

265 _theme: SyntaxTheme 

266 

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 

278 

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) 

313 

314 self._theme = self.get_theme(theme) 

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

316 

317 padding = PaddingProperty() 

318 

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. 

339 

340 Args: 

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

342 encoding (str): Encoding of file. 

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

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

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

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

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

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

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

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

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

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

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

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

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

356 

357 Returns: 

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

359 """ 

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

361 

362 if not lexer: 

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

364 

365 return cls( 

366 code, 

367 lexer, 

368 theme=theme, 

369 dedent=dedent, 

370 line_numbers=line_numbers, 

371 line_range=line_range, 

372 start_line=start_line, 

373 highlight_lines=highlight_lines, 

374 code_width=code_width, 

375 tab_size=tab_size, 

376 word_wrap=word_wrap, 

377 background_color=background_color, 

378 indent_guides=indent_guides, 

379 padding=padding, 

380 ) 

381 

382 @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.. 

390 

391 Args: 

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

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

394 is found for the supplied path. 

395 

396 Returns: 

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

398 """ 

399 lexer: Optional[Lexer] = None 

400 lexer_name = "default" 

401 if code: 

402 try: 

403 lexer = guess_lexer_for_filename(path, code) 

404 except ClassNotFound: 

405 pass 

406 

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 

415 

416 if lexer: 

417 if lexer.aliases: 

418 lexer_name = lexer.aliases[0] 

419 else: 

420 lexer_name = lexer.name 

421 

422 return lexer_name 

423 

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 

428 

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

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

431 

432 Args: 

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

434 

435 Returns: 

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

437 """ 

438 style = self._theme.get_style_for_token(token_type) 

439 return style.color 

440 

441 @property 

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

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

444 

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

446 """ 

447 

448 if isinstance(self._lexer, Lexer): 

449 return self._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 

459 

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 ) 

469 

470 def highlight( 

471 self, 

472 code: str, 

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

474 ) -> Text: 

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

476 

477 Args: 

478 code (str): Code to highlight. 

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

480 

481 Returns: 

482 Text: A text instance containing highlighted syntax. 

483 """ 

484 

485 base_style = self._get_base_style() 

486 justify: JustifyMethod = ( 

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

488 ) 

489 

490 text = Text( 

491 justify=justify, 

492 style=base_style, 

493 tab_size=self.tab_size, 

494 no_wrap=not self.word_wrap, 

495 ) 

496 _get_theme_style = self._theme.get_style_for_token 

497 

498 lexer = self.lexer or self.default_lexer 

499 

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 

507 

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 

511 

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 

516 

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 

522 

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 

539 

540 text.append_tokens(tokens_to_spans()) 

541 

542 else: 

543 text.append_tokens( 

544 (token, _get_theme_style(token_type)) 

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

546 ) 

547 if self.background_color is not None: 

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

549 

550 if self._stylized_ranges: 

551 self._apply_stylized_ranges(text) 

552 

553 return text 

554 

555 def stylize_range( 

556 self, 

557 style: StyleType, 

558 start: SyntaxPosition, 

559 end: SyntaxPosition, 

560 style_before: bool = False, 

561 ) -> None: 

562 """ 

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

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

565 

566 Args: 

567 style (StyleType): The style to apply. 

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

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

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

571 """ 

572 self._stylized_ranges.append( 

573 _SyntaxHighlightRange(style, start, end, style_before) 

574 ) 

575 

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

577 background_style = self._theme.get_background_style() + self.background_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) 

590 

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 

601 

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 

624 

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) 

642 

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 

651 

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) 

674 

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

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

677 

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

679 if not ends_on_nl: 

680 text.remove_suffix("\n") 

681 # Simple case of just rendering 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 

706 

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] 

716 

717 if self.indent_guides and not options.ascii_only: 

718 style = ( 

719 self._get_base_style() 

720 + self._theme.get_style_for_token(Comment) 

721 + Style(dim=True) 

722 + self.background_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 ) 

730 

731 numbers_column_width = self._numbers_column_width 

732 render_options = options.update(width=code_width) 

733 

734 highlight_line = self.highlight_lines.__contains__ 

735 _Segment = Segment 

736 new_line = _Segment("\n") 

737 

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

739 

740 ( 

741 background_style, 

742 number_style, 

743 highlight_number_style, 

744 ) = self._get_number_styles(console) 

745 

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

747 if self.word_wrap: 

748 wrapped_lines = console.render_lines( 

749 line, 

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

751 style=background_style, 

752 pad=not transparent_background, 

753 ) 

754 else: 

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

756 if options.no_wrap: 

757 wrapped_lines = [segments] 

758 else: 

759 wrapped_lines = [ 

760 _Segment.adjust_line_length( 

761 segments, 

762 render_options.max_width, 

763 style=background_style, 

764 pad=not transparent_background, 

765 ) 

766 ] 

767 

768 if self.line_numbers: 

769 wrapped_line_left_pad = _Segment( 

770 " " * numbers_column_width + " ", background_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 

789 

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

791 """ 

792 Apply stylized ranges to a text instance, 

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

794 

795 Args: 

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

797 """ 

798 code = text.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 ] 

809 

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) 

822 

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.) 

827 

828 Args: 

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

830 

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 

842 

843 

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. 

849 

850 Args: 

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

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

853 

854 Returns: 

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

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

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

858 """ 

859 lines_count = len(newlines_offsets) 

860 

861 line_number, column_index = 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 

869 

870 

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

872 import argparse 

873 import sys 

874 

875 parser = argparse.ArgumentParser( 

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

877 ) 

878 parser.add_argument( 

879 "path", 

880 metavar="PATH", 

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

882 ) 

883 parser.add_argument( 

884 "-c", 

885 "--force-color", 

886 dest="force_color", 

887 action="store_true", 

888 default=None, 

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

890 ) 

891 parser.add_argument( 

892 "-i", 

893 "--indent-guides", 

894 dest="indent_guides", 

895 action="store_true", 

896 default=False, 

897 help="display indent guides", 

898 ) 

899 parser.add_argument( 

900 "-l", 

901 "--line-numbers", 

902 dest="line_numbers", 

903 action="store_true", 

904 help="render line numbers", 

905 ) 

906 parser.add_argument( 

907 "-w", 

908 "--width", 

909 type=int, 

910 dest="width", 

911 default=None, 

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

913 ) 

914 parser.add_argument( 

915 "-r", 

916 "--wrap", 

917 dest="word_wrap", 

918 action="store_true", 

919 default=False, 

920 help="word wrap long lines", 

921 ) 

922 parser.add_argument( 

923 "-s", 

924 "--soft-wrap", 

925 action="store_true", 

926 dest="soft_wrap", 

927 default=False, 

928 help="enable soft wrapping mode", 

929 ) 

930 parser.add_argument( 

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

932 ) 

933 parser.add_argument( 

934 "-b", 

935 "--background-color", 

936 dest="background_color", 

937 default=None, 

938 help="Override background color", 

939 ) 

940 parser.add_argument( 

941 "-x", 

942 "--lexer", 

943 default=None, 

944 dest="lexer_name", 

945 help="Lexer name", 

946 ) 

947 parser.add_argument( 

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

949 ) 

950 parser.add_argument( 

951 "--highlight-line", 

952 type=int, 

953 default=None, 

954 dest="highlight_line", 

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

956 ) 

957 args = parser.parse_args() 

958 

959 from rich.console import Console 

960 

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

962 

963 if args.path == "-": 

964 code = sys.stdin.read() 

965 syntax = Syntax( 

966 code=code, 

967 lexer=args.lexer_name, 

968 line_numbers=args.line_numbers, 

969 word_wrap=args.word_wrap, 

970 theme=args.theme, 

971 background_color=args.background_color, 

972 indent_guides=args.indent_guides, 

973 padding=args.padding, 

974 highlight_lines={args.highlight_line}, 

975 ) 

976 else: 

977 syntax = Syntax.from_path( 

978 args.path, 

979 lexer=args.lexer_name, 

980 line_numbers=args.line_numbers, 

981 word_wrap=args.word_wrap, 

982 theme=args.theme, 

983 background_color=args.background_color, 

984 indent_guides=args.indent_guides, 

985 padding=args.padding, 

986 highlight_lines={args.highlight_line}, 

987 ) 

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