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

371 statements  

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

1from __future__ import annotations 

2 

3import sys 

4from typing import ClassVar, Dict, Iterable, List, Optional, Type, Union 

5 

6from markdown_it import MarkdownIt 

7from markdown_it.token import Token 

8 

9if sys.version_info >= (3, 8): 

10 from typing import get_args 

11else: 

12 from typing_extensions import get_args # pragma: no cover 

13 

14from rich.table import Table 

15 

16from . import box 

17from ._loop import loop_first 

18from ._stack import Stack 

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

20from .containers import Renderables 

21from .jupyter import JupyterMixin 

22from .panel import Panel 

23from .rule import Rule 

24from .segment import Segment 

25from .style import Style, StyleStack 

26from .syntax import Syntax 

27from .text import Text, TextType 

28 

29 

30class MarkdownElement: 

31 new_line: ClassVar[bool] = True 

32 

33 @classmethod 

34 def create(cls, markdown: "Markdown", token: Token) -> "MarkdownElement": 

35 """Factory to create markdown element, 

36 

37 Args: 

38 markdown (Markdown): The parent Markdown object. 

39 token (Token): A node from markdown-it. 

40 

41 Returns: 

42 MarkdownElement: A new markdown element 

43 """ 

44 return cls() 

45 

46 def on_enter(self, context: "MarkdownContext") -> None: 

47 """Called when the node is entered. 

48 

49 Args: 

50 context (MarkdownContext): The markdown context. 

51 """ 

52 

53 def on_text(self, context: "MarkdownContext", text: TextType) -> None: 

54 """Called when text is parsed. 

55 

56 Args: 

57 context (MarkdownContext): The markdown context. 

58 """ 

59 

60 def on_leave(self, context: "MarkdownContext") -> None: 

61 """Called when the parser leaves the element. 

62 

63 Args: 

64 context (MarkdownContext): [description] 

65 """ 

66 

67 def on_child_close( 

68 self, context: "MarkdownContext", child: "MarkdownElement" 

69 ) -> bool: 

70 """Called when a child element is closed. 

71 

72 This method allows a parent element to take over rendering of its children. 

73 

74 Args: 

75 context (MarkdownContext): The markdown context. 

76 child (MarkdownElement): The child markdown element. 

77 

78 Returns: 

79 bool: Return True to render the element, or False to not render the element. 

80 """ 

81 return True 

82 

83 def __rich_console__( 

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

85 ) -> "RenderResult": 

86 return () 

87 

88 

89class UnknownElement(MarkdownElement): 

90 """An unknown element. 

91 

92 Hopefully there will be no unknown elements, and we will have a MarkdownElement for 

93 everything in the document. 

94 

95 """ 

96 

97 

98class TextElement(MarkdownElement): 

99 """Base class for elements that render text.""" 

100 

101 style_name = "none" 

102 

103 def on_enter(self, context: "MarkdownContext") -> None: 

104 self.style = context.enter_style(self.style_name) 

105 self.text = Text(justify="left") 

106 

107 def on_text(self, context: "MarkdownContext", text: TextType) -> None: 

108 self.text.append(text, context.current_style if isinstance(text, str) else None) 

109 

110 def on_leave(self, context: "MarkdownContext") -> None: 

111 context.leave_style() 

112 

113 

114class Paragraph(TextElement): 

115 """A Paragraph.""" 

116 

117 style_name = "markdown.paragraph" 

118 justify: JustifyMethod 

119 

120 @classmethod 

121 def create(cls, markdown: "Markdown", token: Token) -> "Paragraph": 

122 return cls(justify=markdown.justify or "left") 

123 

124 def __init__(self, justify: JustifyMethod) -> None: 

125 self.justify = justify 

126 

127 def __rich_console__( 

128 self, console: Console, options: ConsoleOptions 

129 ) -> RenderResult: 

130 self.text.justify = self.justify 

131 yield self.text 

132 

133 

134class Heading(TextElement): 

135 """A heading.""" 

136 

137 @classmethod 

138 def create(cls, markdown: "Markdown", token: Token) -> "Heading": 

139 return cls(token.tag) 

140 

141 def on_enter(self, context: "MarkdownContext") -> None: 

142 self.text = Text() 

143 context.enter_style(self.style_name) 

144 

145 def __init__(self, tag: str) -> None: 

146 self.tag = tag 

147 self.style_name = f"markdown.{tag}" 

148 super().__init__() 

149 

150 def __rich_console__( 

151 self, console: Console, options: ConsoleOptions 

152 ) -> RenderResult: 

153 text = self.text 

154 text.justify = "center" 

155 if self.tag == "h1": 

156 # Draw a border around h1s 

157 yield Panel( 

158 text, 

159 box=box.HEAVY, 

160 style="markdown.h1.border", 

161 ) 

162 else: 

163 # Styled text for h2 and beyond 

164 if self.tag == "h2": 

165 yield Text("") 

166 yield text 

167 

168 

169class CodeBlock(TextElement): 

170 """A code block with syntax highlighting.""" 

171 

172 style_name = "markdown.code_block" 

173 

174 @classmethod 

175 def create(cls, markdown: "Markdown", token: Token) -> "CodeBlock": 

176 node_info = token.info or "" 

177 lexer_name = node_info.partition(" ")[0] 

178 return cls(lexer_name or "default", markdown.code_theme) 

179 

180 def __init__(self, lexer_name: str, theme: str) -> None: 

181 self.lexer_name = lexer_name 

182 self.theme = theme 

183 

184 def __rich_console__( 

185 self, console: Console, options: ConsoleOptions 

186 ) -> RenderResult: 

187 code = str(self.text).rstrip() 

188 syntax = Syntax( 

189 code, self.lexer_name, theme=self.theme, word_wrap=True, padding=1 

190 ) 

191 yield syntax 

192 

193 

194class BlockQuote(TextElement): 

195 """A block quote.""" 

196 

197 style_name = "markdown.block_quote" 

198 

199 def __init__(self) -> None: 

200 self.elements: Renderables = Renderables() 

201 

202 def on_child_close( 

203 self, context: "MarkdownContext", child: "MarkdownElement" 

204 ) -> bool: 

205 self.elements.append(child) 

206 return False 

207 

208 def __rich_console__( 

209 self, console: Console, options: ConsoleOptions 

210 ) -> RenderResult: 

211 render_options = options.update(width=options.max_width - 4) 

212 lines = console.render_lines(self.elements, render_options, style=self.style) 

213 style = self.style 

214 new_line = Segment("\n") 

215 padding = Segment("▌ ", style) 

216 for line in lines: 

217 yield padding 

218 yield from line 

219 yield new_line 

220 

221 

222class HorizontalRule(MarkdownElement): 

223 """A horizontal rule to divide sections.""" 

224 

225 new_line = False 

226 

227 def __rich_console__( 

228 self, console: Console, options: ConsoleOptions 

229 ) -> RenderResult: 

230 style = console.get_style("markdown.hr", default="none") 

231 yield Rule(style=style) 

232 

233 

234class TableElement(MarkdownElement): 

235 """MarkdownElement corresponding to `table_open`.""" 

236 

237 def __init__(self) -> None: 

238 self.header: TableHeaderElement | None = None 

239 self.body: TableBodyElement | None = None 

240 

241 def on_child_close( 

242 self, context: "MarkdownContext", child: "MarkdownElement" 

243 ) -> bool: 

244 if isinstance(child, TableHeaderElement): 

245 self.header = child 

246 elif isinstance(child, TableBodyElement): 

247 self.body = child 

248 else: 

249 raise RuntimeError("Couldn't process markdown table.") 

250 return False 

251 

252 def __rich_console__( 

253 self, console: Console, options: ConsoleOptions 

254 ) -> RenderResult: 

255 table = Table(box=box.SIMPLE_HEAVY) 

256 

257 assert self.header is not None 

258 assert self.header.row is not None 

259 for column in self.header.row.cells: 

260 table.add_column(column.content) 

261 

262 assert self.body is not None 

263 for row in self.body.rows: 

264 row_content = [element.content for element in row.cells] 

265 table.add_row(*row_content) 

266 

267 yield table 

268 

269 

270class TableHeaderElement(MarkdownElement): 

271 """MarkdownElement corresponding to `thead_open` and `thead_close`.""" 

272 

273 def __init__(self) -> None: 

274 self.row: TableRowElement | None = None 

275 

276 def on_child_close( 

277 self, context: "MarkdownContext", child: "MarkdownElement" 

278 ) -> bool: 

279 assert isinstance(child, TableRowElement) 

280 self.row = child 

281 return False 

282 

283 

284class TableBodyElement(MarkdownElement): 

285 """MarkdownElement corresponding to `tbody_open` and `tbody_close`.""" 

286 

287 def __init__(self) -> None: 

288 self.rows: list[TableRowElement] = [] 

289 

290 def on_child_close( 

291 self, context: "MarkdownContext", child: "MarkdownElement" 

292 ) -> bool: 

293 assert isinstance(child, TableRowElement) 

294 self.rows.append(child) 

295 return False 

296 

297 

298class TableRowElement(MarkdownElement): 

299 """MarkdownElement corresponding to `tr_open` and `tr_close`.""" 

300 

301 def __init__(self) -> None: 

302 self.cells: List[TableDataElement] = [] 

303 

304 def on_child_close( 

305 self, context: "MarkdownContext", child: "MarkdownElement" 

306 ) -> bool: 

307 assert isinstance(child, TableDataElement) 

308 self.cells.append(child) 

309 return False 

310 

311 

312class TableDataElement(MarkdownElement): 

313 """MarkdownElement corresponding to `td_open` and `td_close` 

314 and `th_open` and `th_close`.""" 

315 

316 @classmethod 

317 def create(cls, markdown: "Markdown", token: Token) -> "MarkdownElement": 

318 style = str(token.attrs.get("style" "")) or "" 

319 

320 justify: JustifyMethod 

321 if "text-align:right" in style: 

322 justify = "right" 

323 elif "text-align:center" in style: 

324 justify = "center" 

325 elif "text-align:left" in style: 

326 justify = "left" 

327 else: 

328 justify = "default" 

329 

330 assert justify in get_args(JustifyMethod) 

331 return cls(justify=justify) 

332 

333 def __init__(self, justify: JustifyMethod) -> None: 

334 self.content: TextType = "" 

335 self.justify = justify 

336 

337 def on_text(self, context: "MarkdownContext", text: TextType) -> None: 

338 plain = text.plain if isinstance(text, Text) else text 

339 style = text.style if isinstance(text, Text) else "" 

340 self.content = Text( 

341 plain, justify=self.justify, style=context.style_stack.current 

342 ) 

343 

344 

345class ListElement(MarkdownElement): 

346 """A list element.""" 

347 

348 @classmethod 

349 def create(cls, markdown: "Markdown", token: Token) -> "ListElement": 

350 return cls(token.type, int(token.attrs.get("start", 1))) 

351 

352 def __init__(self, list_type: str, list_start: int | None) -> None: 

353 self.items: List[ListItem] = [] 

354 self.list_type = list_type 

355 self.list_start = list_start 

356 

357 def on_child_close( 

358 self, context: "MarkdownContext", child: "MarkdownElement" 

359 ) -> bool: 

360 assert isinstance(child, ListItem) 

361 self.items.append(child) 

362 return False 

363 

364 def __rich_console__( 

365 self, console: Console, options: ConsoleOptions 

366 ) -> RenderResult: 

367 if self.list_type == "bullet_list_open": 

368 for item in self.items: 

369 yield from item.render_bullet(console, options) 

370 else: 

371 number = 1 if self.list_start is None else self.list_start 

372 last_number = number + len(self.items) 

373 for index, item in enumerate(self.items): 

374 yield from item.render_number( 

375 console, options, number + index, last_number 

376 ) 

377 

378 

379class ListItem(TextElement): 

380 """An item in a list.""" 

381 

382 style_name = "markdown.item" 

383 

384 def __init__(self) -> None: 

385 self.elements: Renderables = Renderables() 

386 

387 def on_child_close( 

388 self, context: "MarkdownContext", child: "MarkdownElement" 

389 ) -> bool: 

390 self.elements.append(child) 

391 return False 

392 

393 def render_bullet(self, console: Console, options: ConsoleOptions) -> RenderResult: 

394 render_options = options.update(width=options.max_width - 3) 

395 lines = console.render_lines(self.elements, render_options, style=self.style) 

396 bullet_style = console.get_style("markdown.item.bullet", default="none") 

397 

398 bullet = Segment(" • ", bullet_style) 

399 padding = Segment(" " * 3, bullet_style) 

400 new_line = Segment("\n") 

401 for first, line in loop_first(lines): 

402 yield bullet if first else padding 

403 yield from line 

404 yield new_line 

405 

406 def render_number( 

407 self, console: Console, options: ConsoleOptions, number: int, last_number: int 

408 ) -> RenderResult: 

409 number_width = len(str(last_number)) + 2 

410 render_options = options.update(width=options.max_width - number_width) 

411 lines = console.render_lines(self.elements, render_options, style=self.style) 

412 number_style = console.get_style("markdown.item.number", default="none") 

413 

414 new_line = Segment("\n") 

415 padding = Segment(" " * number_width, number_style) 

416 numeral = Segment(f"{number}".rjust(number_width - 1) + " ", number_style) 

417 for first, line in loop_first(lines): 

418 yield numeral if first else padding 

419 yield from line 

420 yield new_line 

421 

422 

423class Link(TextElement): 

424 @classmethod 

425 def create(cls, markdown: "Markdown", token: Token) -> "MarkdownElement": 

426 url = token.attrs.get("href", "#") 

427 return cls(token.content, str(url)) 

428 

429 def __init__(self, text: str, href: str): 

430 self.text = Text(text) 

431 self.href = href 

432 

433 

434class ImageItem(TextElement): 

435 """Renders a placeholder for an image.""" 

436 

437 new_line = False 

438 

439 @classmethod 

440 def create(cls, markdown: "Markdown", token: Token) -> "MarkdownElement": 

441 """Factory to create markdown element, 

442 

443 Args: 

444 markdown (Markdown): The parent Markdown object. 

445 token (Any): A token from markdown-it. 

446 

447 Returns: 

448 MarkdownElement: A new markdown element 

449 """ 

450 return cls(str(token.attrs.get("src", "")), markdown.hyperlinks) 

451 

452 def __init__(self, destination: str, hyperlinks: bool) -> None: 

453 self.destination = destination 

454 self.hyperlinks = hyperlinks 

455 self.link: Optional[str] = None 

456 super().__init__() 

457 

458 def on_enter(self, context: "MarkdownContext") -> None: 

459 self.link = context.current_style.link 

460 self.text = Text(justify="left") 

461 super().on_enter(context) 

462 

463 def __rich_console__( 

464 self, console: Console, options: ConsoleOptions 

465 ) -> RenderResult: 

466 link_style = Style(link=self.link or self.destination or None) 

467 title = self.text or Text(self.destination.strip("/").rsplit("/", 1)[-1]) 

468 if self.hyperlinks: 

469 title.stylize(link_style) 

470 text = Text.assemble("🌆 ", title, " ", end="") 

471 yield text 

472 

473 

474class MarkdownContext: 

475 """Manages the console render state.""" 

476 

477 def __init__( 

478 self, 

479 console: Console, 

480 options: ConsoleOptions, 

481 style: Style, 

482 inline_code_lexer: Optional[str] = None, 

483 inline_code_theme: str = "monokai", 

484 ) -> None: 

485 self.console = console 

486 self.options = options 

487 self.style_stack: StyleStack = StyleStack(style) 

488 self.stack: Stack[MarkdownElement] = Stack() 

489 

490 self._syntax: Optional[Syntax] = None 

491 if inline_code_lexer is not None: 

492 self._syntax = Syntax("", inline_code_lexer, theme=inline_code_theme) 

493 

494 @property 

495 def current_style(self) -> Style: 

496 """Current style which is the product of all styles on the stack.""" 

497 return self.style_stack.current 

498 

499 def on_text(self, text: str, node_type: str) -> None: 

500 """Called when the parser visits text.""" 

501 if node_type in {"fence", "code_inline"} and self._syntax is not None: 

502 highlight_text = self._syntax.highlight(text) 

503 highlight_text.rstrip() 

504 self.stack.top.on_text( 

505 self, Text.assemble(highlight_text, style=self.style_stack.current) 

506 ) 

507 else: 

508 self.stack.top.on_text(self, text) 

509 

510 def enter_style(self, style_name: Union[str, Style]) -> Style: 

511 """Enter a style context.""" 

512 style = self.console.get_style(style_name, default="none") 

513 self.style_stack.push(style) 

514 return self.current_style 

515 

516 def leave_style(self) -> Style: 

517 """Leave a style context.""" 

518 style = self.style_stack.pop() 

519 return style 

520 

521 

522class Markdown(JupyterMixin): 

523 """A Markdown renderable. 

524 

525 Args: 

526 markup (str): A string containing markdown. 

527 code_theme (str, optional): Pygments theme for code blocks. Defaults to "monokai". 

528 justify (JustifyMethod, optional): Justify value for paragraphs. Defaults to None. 

529 style (Union[str, Style], optional): Optional style to apply to markdown. 

530 hyperlinks (bool, optional): Enable hyperlinks. Defaults to ``True``. 

531 inline_code_lexer: (str, optional): Lexer to use if inline code highlighting is 

532 enabled. Defaults to None. 

533 inline_code_theme: (Optional[str], optional): Pygments theme for inline code 

534 highlighting, or None for no highlighting. Defaults to None. 

535 """ 

536 

537 elements: ClassVar[Dict[str, Type[MarkdownElement]]] = { 

538 "paragraph_open": Paragraph, 

539 "heading_open": Heading, 

540 "fence": CodeBlock, 

541 "code_block": CodeBlock, 

542 "blockquote_open": BlockQuote, 

543 "hr": HorizontalRule, 

544 "bullet_list_open": ListElement, 

545 "ordered_list_open": ListElement, 

546 "list_item_open": ListItem, 

547 "image": ImageItem, 

548 "table_open": TableElement, 

549 "tbody_open": TableBodyElement, 

550 "thead_open": TableHeaderElement, 

551 "tr_open": TableRowElement, 

552 "td_open": TableDataElement, 

553 "th_open": TableDataElement, 

554 } 

555 

556 inlines = {"em", "strong", "code", "s"} 

557 

558 def __init__( 

559 self, 

560 markup: str, 

561 code_theme: str = "monokai", 

562 justify: Optional[JustifyMethod] = None, 

563 style: Union[str, Style] = "none", 

564 hyperlinks: bool = True, 

565 inline_code_lexer: Optional[str] = None, 

566 inline_code_theme: Optional[str] = None, 

567 ) -> None: 

568 parser = MarkdownIt().enable("strikethrough").enable("table") 

569 self.markup = markup 

570 self.parsed = parser.parse(markup) 

571 self.code_theme = code_theme 

572 self.justify: Optional[JustifyMethod] = justify 

573 self.style = style 

574 self.hyperlinks = hyperlinks 

575 self.inline_code_lexer = inline_code_lexer 

576 self.inline_code_theme = inline_code_theme or code_theme 

577 

578 def _flatten_tokens(self, tokens: Iterable[Token]) -> Iterable[Token]: 

579 """Flattens the token stream.""" 

580 for token in tokens: 

581 is_fence = token.type == "fence" 

582 is_image = token.tag == "img" 

583 if token.children and not (is_image or is_fence): 

584 yield from self._flatten_tokens(token.children) 

585 else: 

586 yield token 

587 

588 def __rich_console__( 

589 self, console: Console, options: ConsoleOptions 

590 ) -> RenderResult: 

591 """Render markdown to the console.""" 

592 style = console.get_style(self.style, default="none") 

593 options = options.update(height=None) 

594 context = MarkdownContext( 

595 console, 

596 options, 

597 style, 

598 inline_code_lexer=self.inline_code_lexer, 

599 inline_code_theme=self.inline_code_theme, 

600 ) 

601 tokens = self.parsed 

602 inline_style_tags = self.inlines 

603 new_line = False 

604 _new_line_segment = Segment.line() 

605 

606 for token in self._flatten_tokens(tokens): 

607 node_type = token.type 

608 tag = token.tag 

609 

610 entering = token.nesting == 1 

611 exiting = token.nesting == -1 

612 self_closing = token.nesting == 0 

613 

614 if node_type == "text": 

615 context.on_text(token.content, node_type) 

616 elif node_type == "hardbreak": 

617 context.on_text("\n", node_type) 

618 elif node_type == "softbreak": 

619 context.on_text(" ", node_type) 

620 elif node_type == "link_open": 

621 href = str(token.attrs.get("href", "")) 

622 if self.hyperlinks: 

623 link_style = console.get_style("markdown.link_url", default="none") 

624 link_style += Style(link=href) 

625 context.enter_style(link_style) 

626 else: 

627 context.stack.push(Link.create(self, token)) 

628 elif node_type == "link_close": 

629 if self.hyperlinks: 

630 context.leave_style() 

631 else: 

632 element = context.stack.pop() 

633 assert isinstance(element, Link) 

634 link_style = console.get_style("markdown.link", default="none") 

635 context.enter_style(link_style) 

636 context.on_text(element.text.plain, node_type) 

637 context.leave_style() 

638 context.on_text(" (", node_type) 

639 link_url_style = console.get_style( 

640 "markdown.link_url", default="none" 

641 ) 

642 context.enter_style(link_url_style) 

643 context.on_text(element.href, node_type) 

644 context.leave_style() 

645 context.on_text(")", node_type) 

646 elif ( 

647 tag in inline_style_tags 

648 and node_type != "fence" 

649 and node_type != "code_block" 

650 ): 

651 if entering: 

652 # If it's an opening inline token e.g. strong, em, etc. 

653 # Then we move into a style context i.e. push to stack. 

654 context.enter_style(f"markdown.{tag}") 

655 elif exiting: 

656 # If it's a closing inline style, then we pop the style 

657 # off of the stack, to move out of the context of it... 

658 context.leave_style() 

659 else: 

660 # If it's a self-closing inline style e.g. `code_inline` 

661 context.enter_style(f"markdown.{tag}") 

662 if token.content: 

663 context.on_text(token.content, node_type) 

664 context.leave_style() 

665 else: 

666 # Map the markdown tag -> MarkdownElement renderable 

667 element_class = self.elements.get(token.type) or UnknownElement 

668 element = element_class.create(self, token) 

669 

670 if entering or self_closing: 

671 context.stack.push(element) 

672 element.on_enter(context) 

673 

674 if exiting: # CLOSING tag 

675 element = context.stack.pop() 

676 

677 should_render = not context.stack or ( 

678 context.stack 

679 and context.stack.top.on_child_close(context, element) 

680 ) 

681 

682 if should_render: 

683 if new_line: 

684 yield _new_line_segment 

685 

686 yield from console.render(element, context.options) 

687 elif self_closing: # SELF-CLOSING tags (e.g. text, code, image) 

688 context.stack.pop() 

689 text = token.content 

690 if text is not None: 

691 element.on_text(context, text) 

692 

693 should_render = ( 

694 not context.stack 

695 or context.stack 

696 and context.stack.top.on_child_close(context, element) 

697 ) 

698 if should_render: 

699 if new_line: 

700 yield _new_line_segment 

701 yield from console.render(element, context.options) 

702 

703 if exiting or self_closing: 

704 element.on_leave(context) 

705 new_line = element.new_line 

706 

707 

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

709 import argparse 

710 import sys 

711 

712 parser = argparse.ArgumentParser( 

713 description="Render Markdown to the console with Rich" 

714 ) 

715 parser.add_argument( 

716 "path", 

717 metavar="PATH", 

718 help="path to markdown file, or - for stdin", 

719 ) 

720 parser.add_argument( 

721 "-c", 

722 "--force-color", 

723 dest="force_color", 

724 action="store_true", 

725 default=None, 

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

727 ) 

728 parser.add_argument( 

729 "-t", 

730 "--code-theme", 

731 dest="code_theme", 

732 default="monokai", 

733 help="pygments code theme", 

734 ) 

735 parser.add_argument( 

736 "-i", 

737 "--inline-code-lexer", 

738 dest="inline_code_lexer", 

739 default=None, 

740 help="inline_code_lexer", 

741 ) 

742 parser.add_argument( 

743 "-y", 

744 "--hyperlinks", 

745 dest="hyperlinks", 

746 action="store_true", 

747 help="enable hyperlinks", 

748 ) 

749 parser.add_argument( 

750 "-w", 

751 "--width", 

752 type=int, 

753 dest="width", 

754 default=None, 

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

756 ) 

757 parser.add_argument( 

758 "-j", 

759 "--justify", 

760 dest="justify", 

761 action="store_true", 

762 help="enable full text justify", 

763 ) 

764 parser.add_argument( 

765 "-p", 

766 "--page", 

767 dest="page", 

768 action="store_true", 

769 help="use pager to scroll output", 

770 ) 

771 args = parser.parse_args() 

772 

773 from rich.console import Console 

774 

775 if args.path == "-": 

776 markdown_body = sys.stdin.read() 

777 else: 

778 with open(args.path, "rt", encoding="utf-8") as markdown_file: 

779 markdown_body = markdown_file.read() 

780 

781 markdown = Markdown( 

782 markdown_body, 

783 justify="full" if args.justify else "left", 

784 code_theme=args.code_theme, 

785 hyperlinks=args.hyperlinks, 

786 inline_code_lexer=args.inline_code_lexer, 

787 ) 

788 if args.page: 

789 import io 

790 import pydoc 

791 

792 fileio = io.StringIO() 

793 console = Console( 

794 file=fileio, force_terminal=args.force_color, width=args.width 

795 ) 

796 console.print(markdown) 

797 pydoc.pager(fileio.getvalue()) 

798 

799 else: 

800 console = Console( 

801 force_terminal=args.force_color, width=args.width, record=True 

802 ) 

803 console.print(markdown)