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

329 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:07 +0000

1import os.path 

2import platform 

3import re 

4import sys 

5import textwrap 

6from abc import ABC, abstractmethod 

7from pathlib import Path 

8from typing import ( 

9 Any, 

10 Dict, 

11 Iterable, 

12 List, 

13 NamedTuple, 

14 Optional, 

15 Sequence, 

16 Set, 

17 Tuple, 

18 Type, 

19 Union, 

20) 

21 

22from pygments.lexer import Lexer 

23from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename 

24from pygments.style import Style as PygmentsStyle 

25from pygments.styles import get_style_by_name 

26from pygments.token import ( 

27 Comment, 

28 Error, 

29 Generic, 

30 Keyword, 

31 Name, 

32 Number, 

33 Operator, 

34 String, 

35 Token, 

36 Whitespace, 

37) 

38from pygments.util import ClassNotFound 

39 

40from rich.containers import Lines 

41from rich.padding import Padding, PaddingDimensions 

42 

43from ._loop import loop_first 

44from .cells import cell_len 

45from .color import Color, blend_rgb 

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

47from .jupyter import JupyterMixin 

48from .measure import Measurement 

49from .segment import Segment, Segments 

50from .style import Style, StyleType 

51from .text import Text 

52 

53TokenType = Tuple[str, ...] 

54 

55WINDOWS = platform.system() == "Windows" 

56DEFAULT_THEME = "monokai" 

57 

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

59# A few modifications were made 

60 

61ANSI_LIGHT: Dict[TokenType, Style] = { 

62 Token: Style(), 

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

64 Comment: Style(dim=True), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

88} 

89 

90ANSI_DARK: Dict[TokenType, Style] = { 

91 Token: Style(), 

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

93 Comment: Style(dim=True), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

117} 

118 

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

120NUMBERS_COLUMN_DEFAULT_PADDING = 2 

121 

122 

123class SyntaxTheme(ABC): 

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

125 

126 @abstractmethod 

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

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

129 raise NotImplementedError # pragma: no cover 

130 

131 @abstractmethod 

132 def get_background_style(self) -> Style: 

133 """Get the background color.""" 

134 raise NotImplementedError # pragma: no cover 

135 

136 

137class PygmentsSyntaxTheme(SyntaxTheme): 

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

139 

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

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

142 if isinstance(theme, str): 

143 try: 

144 self._pygments_style_class = get_style_by_name(theme) 

145 except ClassNotFound: 

146 self._pygments_style_class = get_style_by_name("default") 

147 else: 

148 self._pygments_style_class = theme 

149 

150 self._background_color = self._pygments_style_class.background_color 

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

152 

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

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

155 try: 

156 return self._style_cache[token_type] 

157 except KeyError: 

158 try: 

159 pygments_style = self._pygments_style_class.style_for_token(token_type) 

160 except KeyError: 

161 style = Style.null() 

162 else: 

163 color = pygments_style["color"] 

164 bgcolor = pygments_style["bgcolor"] 

165 style = Style( 

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

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

168 bold=pygments_style["bold"], 

169 italic=pygments_style["italic"], 

170 underline=pygments_style["underline"], 

171 ) 

172 self._style_cache[token_type] = style 

173 return style 

174 

175 def get_background_style(self) -> Style: 

176 return self._background_style 

177 

178 

179class ANSISyntaxTheme(SyntaxTheme): 

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

181 

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

183 self.style_map = style_map 

184 self._missing_style = Style.null() 

185 self._background_style = Style.null() 

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

187 

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

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

190 try: 

191 return self._style_cache[token_type] 

192 except KeyError: 

193 # Styles form a hierarchy 

194 # We need to go from most to least specific 

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

196 get_style = self.style_map.get 

197 token = tuple(token_type) 

198 style = self._missing_style 

199 while token: 

200 _style = get_style(token) 

201 if _style is not None: 

202 style = _style 

203 break 

204 token = token[:-1] 

205 self._style_cache[token_type] = style 

206 return style 

207 

208 def get_background_style(self) -> Style: 

209 return self._background_style 

210 

211 

212SyntaxPosition = Tuple[int, int] 

213 

214 

215class _SyntaxHighlightRange(NamedTuple): 

216 """ 

217 A range to highlight in a Syntax object. 

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

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

220 """ 

221 

222 style: StyleType 

223 start: SyntaxPosition 

224 end: SyntaxPosition 

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 def highlight( 

443 self, 

444 code: str, 

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

446 ) -> Text: 

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

448 

449 Args: 

450 code (str): Code to highlight. 

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

452 

453 Returns: 

454 Text: A text instance containing highlighted syntax. 

455 """ 

456 

457 base_style = self._get_base_style() 

458 justify: JustifyMethod = ( 

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

460 ) 

461 

462 text = Text( 

463 justify=justify, 

464 style=base_style, 

465 tab_size=self.tab_size, 

466 no_wrap=not self.word_wrap, 

467 ) 

468 _get_theme_style = self._theme.get_style_for_token 

469 

470 lexer = self.lexer 

471 

472 if lexer is None: 

473 text.append(code) 

474 else: 

475 if line_range: 

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

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

478 line_start, line_end = line_range 

479 

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

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

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

483 

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

485 while token: 

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

487 yield token_type, line_token + new_line 

488 

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

490 """Convert tokens to spans.""" 

491 tokens = iter(line_tokenize()) 

492 line_no = 0 

493 _line_start = line_start - 1 if line_start else 0 

494 

495 # Skip over tokens until line start 

496 while line_no < _line_start: 

497 try: 

498 _token_type, token = next(tokens) 

499 except StopIteration: 

500 break 

501 yield (token, None) 

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

503 line_no += 1 

504 # Generate spans until line end 

505 for token_type, token in tokens: 

506 yield (token, _get_theme_style(token_type)) 

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

508 line_no += 1 

509 if line_end and line_no >= line_end: 

510 break 

511 

512 text.append_tokens(tokens_to_spans()) 

513 

514 else: 

515 text.append_tokens( 

516 (token, _get_theme_style(token_type)) 

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

518 ) 

519 if self.background_color is not None: 

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

521 

522 if self._stylized_ranges: 

523 self._apply_stylized_ranges(text) 

524 

525 return text 

526 

527 def stylize_range( 

528 self, style: StyleType, start: SyntaxPosition, end: SyntaxPosition 

529 ) -> None: 

530 """ 

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

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

533 

534 Args: 

535 style (StyleType): The style to apply. 

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

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

538 """ 

539 self._stylized_ranges.append(_SyntaxHighlightRange(style, start, end)) 

540 

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

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

543 background_color = background_style.bgcolor 

544 if background_color is None or background_color.is_system_defined: 

545 return Color.default() 

546 foreground_color = self._get_token_color(Token.Text) 

547 if foreground_color is None or foreground_color.is_system_defined: 

548 return foreground_color or Color.default() 

549 new_color = blend_rgb( 

550 background_color.get_truecolor(), 

551 foreground_color.get_truecolor(), 

552 cross_fade=blend, 

553 ) 

554 return Color.from_triplet(new_color) 

555 

556 @property 

557 def _numbers_column_width(self) -> int: 

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

559 column_width = 0 

560 if self.line_numbers: 

561 column_width = ( 

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

563 + NUMBERS_COLUMN_DEFAULT_PADDING 

564 ) 

565 return column_width 

566 

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

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

569 background_style = self._get_base_style() 

570 if background_style.transparent_background: 

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

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

573 number_style = Style.chain( 

574 background_style, 

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

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

577 self.background_style, 

578 ) 

579 highlight_number_style = Style.chain( 

580 background_style, 

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

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

583 self.background_style, 

584 ) 

585 else: 

586 number_style = background_style + Style(dim=True) 

587 highlight_number_style = background_style + Style(dim=False) 

588 return background_style, number_style, highlight_number_style 

589 

590 def __rich_measure__( 

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

592 ) -> "Measurement": 

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

594 padding = left + right 

595 if self.code_width is not None: 

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

597 return Measurement(self._numbers_column_width, width) 

598 lines = self.code.splitlines() 

599 width = ( 

600 self._numbers_column_width 

601 + padding 

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

603 ) 

604 if self.line_numbers: 

605 width += 1 

606 return Measurement(self._numbers_column_width, width) 

607 

608 def __rich_console__( 

609 self, console: Console, options: ConsoleOptions 

610 ) -> RenderResult: 

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

612 if self.padding: 

613 yield Padding( 

614 segments, style=self._theme.get_background_style(), pad=self.padding 

615 ) 

616 else: 

617 yield segments 

618 

619 def _get_syntax( 

620 self, 

621 console: Console, 

622 options: ConsoleOptions, 

623 ) -> Iterable[Segment]: 

624 """ 

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

626 """ 

627 transparent_background = self._get_base_style().transparent_background 

628 code_width = ( 

629 ( 

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

631 if self.line_numbers 

632 else options.max_width 

633 ) 

634 if self.code_width is None 

635 else self.code_width 

636 ) 

637 

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

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

640 

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

642 if not ends_on_nl: 

643 text.remove_suffix("\n") 

644 # Simple case of just rendering text 

645 style = ( 

646 self._get_base_style() 

647 + self._theme.get_style_for_token(Comment) 

648 + Style(dim=True) 

649 + self.background_style 

650 ) 

651 if self.indent_guides and not options.ascii_only: 

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

653 text.overflow = "crop" 

654 if style.transparent_background: 

655 yield from console.render( 

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

657 ) 

658 else: 

659 syntax_lines = console.render_lines( 

660 text, 

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

662 style=self.background_style, 

663 pad=True, 

664 new_lines=True, 

665 ) 

666 for syntax_line in syntax_lines: 

667 yield from syntax_line 

668 return 

669 

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

671 line_offset = 0 

672 if start_line: 

673 line_offset = max(0, start_line - 1) 

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

675 if self.line_range: 

676 if line_offset > len(lines): 

677 return 

678 lines = lines[line_offset:end_line] 

679 

680 if self.indent_guides and not options.ascii_only: 

681 style = ( 

682 self._get_base_style() 

683 + self._theme.get_style_for_token(Comment) 

684 + Style(dim=True) 

685 + self.background_style 

686 ) 

687 lines = ( 

688 Text("\n") 

689 .join(lines) 

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

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

692 ) 

693 

694 numbers_column_width = self._numbers_column_width 

695 render_options = options.update(width=code_width) 

696 

697 highlight_line = self.highlight_lines.__contains__ 

698 _Segment = Segment 

699 new_line = _Segment("\n") 

700 

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

702 

703 ( 

704 background_style, 

705 number_style, 

706 highlight_number_style, 

707 ) = self._get_number_styles(console) 

708 

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

710 if self.word_wrap: 

711 wrapped_lines = console.render_lines( 

712 line, 

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

714 style=background_style, 

715 pad=not transparent_background, 

716 ) 

717 else: 

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

719 if options.no_wrap: 

720 wrapped_lines = [segments] 

721 else: 

722 wrapped_lines = [ 

723 _Segment.adjust_line_length( 

724 segments, 

725 render_options.max_width, 

726 style=background_style, 

727 pad=not transparent_background, 

728 ) 

729 ] 

730 

731 if self.line_numbers: 

732 wrapped_line_left_pad = _Segment( 

733 " " * numbers_column_width + " ", background_style 

734 ) 

735 for first, wrapped_line in loop_first(wrapped_lines): 

736 if first: 

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

738 if highlight_line(line_no): 

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

740 yield _Segment(line_column, highlight_number_style) 

741 else: 

742 yield _Segment(" ", highlight_number_style) 

743 yield _Segment(line_column, number_style) 

744 else: 

745 yield wrapped_line_left_pad 

746 yield from wrapped_line 

747 yield new_line 

748 else: 

749 for wrapped_line in wrapped_lines: 

750 yield from wrapped_line 

751 yield new_line 

752 

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

754 """ 

755 Apply stylized ranges to a text instance, 

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

757 

758 Args: 

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

760 """ 

761 code = text.plain 

762 newlines_offsets = [ 

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

764 0, 

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

766 *[ 

767 match.start() + 1 

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

769 ], 

770 len(code) + 1, 

771 ] 

772 

773 for stylized_range in self._stylized_ranges: 

774 start = _get_code_index_for_syntax_position( 

775 newlines_offsets, stylized_range.start 

776 ) 

777 end = _get_code_index_for_syntax_position( 

778 newlines_offsets, stylized_range.end 

779 ) 

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

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

782 

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

784 """ 

785 Applies various processing to a raw code string 

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

787 

788 Args: 

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

790 

791 Returns: 

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

793 while the string is the processed code. 

794 """ 

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

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

797 processed_code = ( 

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

799 ) 

800 processed_code = processed_code.expandtabs(self.tab_size) 

801 return ends_on_nl, processed_code 

802 

803 

804def _get_code_index_for_syntax_position( 

805 newlines_offsets: Sequence[int], position: SyntaxPosition 

806) -> Optional[int]: 

807 """ 

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

809 

810 Args: 

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

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

813 

814 Returns: 

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

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

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

818 """ 

819 lines_count = len(newlines_offsets) 

820 

821 line_number, column_index = position 

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

823 return None # `line_number` is out of range 

824 line_index = line_number - 1 

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

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

827 column_index = min(line_length, column_index) 

828 return newlines_offsets[line_index] + column_index 

829 

830 

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

832 import argparse 

833 import sys 

834 

835 parser = argparse.ArgumentParser( 

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

837 ) 

838 parser.add_argument( 

839 "path", 

840 metavar="PATH", 

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

842 ) 

843 parser.add_argument( 

844 "-c", 

845 "--force-color", 

846 dest="force_color", 

847 action="store_true", 

848 default=None, 

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

850 ) 

851 parser.add_argument( 

852 "-i", 

853 "--indent-guides", 

854 dest="indent_guides", 

855 action="store_true", 

856 default=False, 

857 help="display indent guides", 

858 ) 

859 parser.add_argument( 

860 "-l", 

861 "--line-numbers", 

862 dest="line_numbers", 

863 action="store_true", 

864 help="render line numbers", 

865 ) 

866 parser.add_argument( 

867 "-w", 

868 "--width", 

869 type=int, 

870 dest="width", 

871 default=None, 

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

873 ) 

874 parser.add_argument( 

875 "-r", 

876 "--wrap", 

877 dest="word_wrap", 

878 action="store_true", 

879 default=False, 

880 help="word wrap long lines", 

881 ) 

882 parser.add_argument( 

883 "-s", 

884 "--soft-wrap", 

885 action="store_true", 

886 dest="soft_wrap", 

887 default=False, 

888 help="enable soft wrapping mode", 

889 ) 

890 parser.add_argument( 

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

892 ) 

893 parser.add_argument( 

894 "-b", 

895 "--background-color", 

896 dest="background_color", 

897 default=None, 

898 help="Override background color", 

899 ) 

900 parser.add_argument( 

901 "-x", 

902 "--lexer", 

903 default=None, 

904 dest="lexer_name", 

905 help="Lexer name", 

906 ) 

907 parser.add_argument( 

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

909 ) 

910 parser.add_argument( 

911 "--highlight-line", 

912 type=int, 

913 default=None, 

914 dest="highlight_line", 

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

916 ) 

917 args = parser.parse_args() 

918 

919 from rich.console import Console 

920 

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

922 

923 if args.path == "-": 

924 code = sys.stdin.read() 

925 syntax = Syntax( 

926 code=code, 

927 lexer=args.lexer_name, 

928 line_numbers=args.line_numbers, 

929 word_wrap=args.word_wrap, 

930 theme=args.theme, 

931 background_color=args.background_color, 

932 indent_guides=args.indent_guides, 

933 padding=args.padding, 

934 highlight_lines={args.highlight_line}, 

935 ) 

936 else: 

937 syntax = Syntax.from_path( 

938 args.path, 

939 lexer=args.lexer_name, 

940 line_numbers=args.line_numbers, 

941 word_wrap=args.word_wrap, 

942 theme=args.theme, 

943 background_color=args.background_color, 

944 indent_guides=args.indent_guides, 

945 padding=args.padding, 

946 highlight_lines={args.highlight_line}, 

947 ) 

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