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

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

373 statements  

1from __future__ import annotations 

2 

3import sys 

4from dataclasses import dataclass 

5from typing import ClassVar, Iterable, get_args 

6 

7from markdown_it import MarkdownIt 

8from markdown_it.token import Token 

9 

10from rich.table import Table 

11 

12from . import box 

13from ._loop import loop_first 

14from ._stack import Stack 

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

16from .containers import Renderables 

17from .jupyter import JupyterMixin 

18from .rule import Rule 

19from .segment import Segment 

20from .style import Style, StyleStack 

21from .syntax import Syntax 

22from .text import Text, TextType 

23 

24 

25class MarkdownElement: 

26 new_line: ClassVar[bool] = True 

27 

28 @classmethod 

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

30 """Factory to create markdown element, 

31 

32 Args: 

33 markdown (Markdown): The parent Markdown object. 

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

35 

36 Returns: 

37 MarkdownElement: A new markdown element 

38 """ 

39 return cls() 

40 

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

42 """Called when the node is entered. 

43 

44 Args: 

45 context (MarkdownContext): The markdown context. 

46 """ 

47 

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

49 """Called when text is parsed. 

50 

51 Args: 

52 context (MarkdownContext): The markdown context. 

53 """ 

54 

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

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

57 

58 Args: 

59 context (MarkdownContext): [description] 

60 """ 

61 

62 def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool: 

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

64 

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

66 

67 Args: 

68 context (MarkdownContext): The markdown context. 

69 child (MarkdownElement): The child markdown element. 

70 

71 Returns: 

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

73 """ 

74 return True 

75 

76 def __rich_console__( 

77 self, console: Console, options: ConsoleOptions 

78 ) -> RenderResult: 

79 return () 

80 

81 

82class UnknownElement(MarkdownElement): 

83 """An unknown element. 

84 

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

86 everything in the document. 

87 

88 """ 

89 

90 

91class TextElement(MarkdownElement): 

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

93 

94 style_name = "none" 

95 

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

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

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

99 

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

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

102 

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

104 context.leave_style() 

105 

106 

107class Paragraph(TextElement): 

108 """A Paragraph.""" 

109 

110 style_name = "markdown.paragraph" 

111 justify: JustifyMethod 

112 

113 @classmethod 

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

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

116 

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

118 self.justify = justify 

119 

120 def __rich_console__( 

121 self, console: Console, options: ConsoleOptions 

122 ) -> RenderResult: 

123 self.text.justify = self.justify 

124 yield self.text 

125 

126 

127@dataclass 

128class HeadingFormat: 

129 justify: JustifyMethod = "left" 

130 style: str = "" 

131 

132 

133class Heading(TextElement): 

134 """A heading.""" 

135 

136 LEVEL_ALIGN: ClassVar[dict[str, JustifyMethod]] = { 

137 "h1": "center", 

138 "h2": "left", 

139 "h3": "left", 

140 "h4": "left", 

141 "h5": "left", 

142 "h6": "left", 

143 } 

144 

145 @classmethod 

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

147 return cls(token.tag) 

148 

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

150 self.text = Text() 

151 context.enter_style(self.style_name) 

152 

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

154 self.tag = tag 

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

156 super().__init__() 

157 

158 def __rich_console__( 

159 self, console: Console, options: ConsoleOptions 

160 ) -> RenderResult: 

161 text = self.text.copy() 

162 heading_justify = self.LEVEL_ALIGN.get(self.tag, "left") 

163 text.justify = heading_justify 

164 yield text 

165 

166 

167class CodeBlock(TextElement): 

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

169 

170 style_name = "markdown.code_block" 

171 

172 @classmethod 

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

174 node_info = token.info or "" 

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

176 return cls(lexer_name or "text", markdown.code_theme) 

177 

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

179 self.lexer_name = lexer_name 

180 self.theme = theme 

181 

182 def __rich_console__( 

183 self, console: Console, options: ConsoleOptions 

184 ) -> RenderResult: 

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

186 syntax = Syntax( 

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

188 ) 

189 yield syntax 

190 

191 

192class BlockQuote(TextElement): 

193 """A block quote.""" 

194 

195 style_name = "markdown.block_quote" 

196 

197 def __init__(self) -> None: 

198 self.elements: Renderables = Renderables() 

199 

200 def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool: 

201 self.elements.append(child) 

202 return False 

203 

204 def __rich_console__( 

205 self, console: Console, options: ConsoleOptions 

206 ) -> RenderResult: 

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

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

209 style = self.style 

210 new_line = Segment("\n") 

211 padding = Segment("▌ ", style) 

212 for line in lines: 

213 yield padding 

214 yield from line 

215 yield new_line 

216 

217 

218class HorizontalRule(MarkdownElement): 

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

220 

221 new_line = False 

222 

223 def __rich_console__( 

224 self, console: Console, options: ConsoleOptions 

225 ) -> RenderResult: 

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

227 yield Rule(style=style, characters="-") 

228 yield Text() 

229 

230 

231class TableElement(MarkdownElement): 

232 """MarkdownElement corresponding to `table_open`.""" 

233 

234 def __init__(self) -> None: 

235 self.header: TableHeaderElement | None = None 

236 self.body: TableBodyElement | None = None 

237 

238 def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool: 

239 if isinstance(child, TableHeaderElement): 

240 self.header = child 

241 elif isinstance(child, TableBodyElement): 

242 self.body = child 

243 else: 

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

245 return False 

246 

247 def __rich_console__( 

248 self, console: Console, options: ConsoleOptions 

249 ) -> RenderResult: 

250 table = Table( 

251 box=box.SIMPLE, 

252 pad_edge=False, 

253 style="markdown.table.border", 

254 show_edge=True, 

255 collapse_padding=True, 

256 ) 

257 

258 if self.header is not None and self.header.row is not None: 

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

260 heading = column.content.copy() 

261 heading.stylize("markdown.table.header") 

262 table.add_column(heading) 

263 

264 if self.body is not None: 

265 for row in self.body.rows: 

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

267 table.add_row(*row_content) 

268 

269 yield table 

270 

271 

272class TableHeaderElement(MarkdownElement): 

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

274 

275 def __init__(self) -> None: 

276 self.row: TableRowElement | None = None 

277 

278 def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> 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(self, context: MarkdownContext, child: MarkdownElement) -> bool: 

291 assert isinstance(child, TableRowElement) 

292 self.rows.append(child) 

293 return False 

294 

295 

296class TableRowElement(MarkdownElement): 

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

298 

299 def __init__(self) -> None: 

300 self.cells: list[TableDataElement] = [] 

301 

302 def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool: 

303 assert isinstance(child, TableDataElement) 

304 self.cells.append(child) 

305 return False 

306 

307 

308class TableDataElement(MarkdownElement): 

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

310 and `th_open` and `th_close`.""" 

311 

312 @classmethod 

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

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

315 

316 justify: JustifyMethod 

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

318 justify = "right" 

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

320 justify = "center" 

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

322 justify = "left" 

323 else: 

324 justify = "default" 

325 

326 assert justify in get_args(JustifyMethod) 

327 return cls(justify=justify) 

328 

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

330 self.content: Text = Text("", justify=justify) 

331 self.justify = justify 

332 

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

334 text = Text(text) if isinstance(text, str) else text 

335 text.stylize(context.current_style) 

336 self.content.append_text(text) 

337 

338 

339class ListElement(MarkdownElement): 

340 """A list element.""" 

341 

342 @classmethod 

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

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

345 

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

347 self.items: list[ListItem] = [] 

348 self.list_type = list_type 

349 self.list_start = list_start 

350 

351 def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool: 

352 assert isinstance(child, ListItem) 

353 self.items.append(child) 

354 return False 

355 

356 def __rich_console__( 

357 self, console: Console, options: ConsoleOptions 

358 ) -> RenderResult: 

359 if self.list_type == "bullet_list_open": 

360 for item in self.items: 

361 yield from item.render_bullet(console, options) 

362 else: 

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

364 last_number = number + len(self.items) 

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

366 yield from item.render_number( 

367 console, options, number + index, last_number 

368 ) 

369 

370 

371class ListItem(TextElement): 

372 """An item in a list.""" 

373 

374 style_name = "markdown.item" 

375 

376 def __init__(self) -> None: 

377 self.elements: Renderables = Renderables() 

378 

379 def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool: 

380 self.elements.append(child) 

381 return False 

382 

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

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

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

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

387 

388 bullet = Segment(" • ", bullet_style) 

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

390 new_line = Segment("\n") 

391 for first, line in loop_first(lines): 

392 yield bullet if first else padding 

393 yield from line 

394 yield new_line 

395 

396 def render_number( 

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

398 ) -> RenderResult: 

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

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

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

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

403 

404 new_line = Segment("\n") 

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

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

407 for first, line in loop_first(lines): 

408 yield numeral if first else padding 

409 yield from line 

410 yield new_line 

411 

412 

413class Link(TextElement): 

414 @classmethod 

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

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

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

418 

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

420 self.text = Text(text) 

421 self.href = href 

422 

423 

424class ImageItem(TextElement): 

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

426 

427 new_line = False 

428 

429 @classmethod 

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

431 """Factory to create markdown element, 

432 

433 Args: 

434 markdown (Markdown): The parent Markdown object. 

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

436 

437 Returns: 

438 MarkdownElement: A new markdown element 

439 """ 

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

441 

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

443 self.destination = destination 

444 self.hyperlinks = hyperlinks 

445 self.link: str | None = None 

446 super().__init__() 

447 

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

449 self.link = context.current_style.link 

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

451 super().on_enter(context) 

452 

453 def __rich_console__( 

454 self, console: Console, options: ConsoleOptions 

455 ) -> RenderResult: 

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

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

458 if self.hyperlinks: 

459 title.stylize(link_style) 

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

461 yield text 

462 

463 

464class MarkdownContext: 

465 """Manages the console render state.""" 

466 

467 def __init__( 

468 self, 

469 console: Console, 

470 options: ConsoleOptions, 

471 style: Style, 

472 inline_code_lexer: str | None = None, 

473 inline_code_theme: str = "monokai", 

474 ) -> None: 

475 self.console = console 

476 self.options = options 

477 self.style_stack: StyleStack = StyleStack(style) 

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

479 

480 self._syntax: Syntax | None = None 

481 if inline_code_lexer is not None: 

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

483 

484 @property 

485 def current_style(self) -> Style: 

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

487 return self.style_stack.current 

488 

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

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

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

492 highlight_text = self._syntax.highlight(text) 

493 highlight_text.rstrip() 

494 self.stack.top.on_text( 

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

496 ) 

497 else: 

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

499 

500 def enter_style(self, style_name: str | Style) -> Style: 

501 """Enter a style context.""" 

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

503 self.style_stack.push(style) 

504 return self.current_style 

505 

506 def leave_style(self) -> Style: 

507 """Leave a style context.""" 

508 style = self.style_stack.pop() 

509 return style 

510 

511 

512class Markdown(JupyterMixin): 

513 """A Markdown renderable. 

514 

515 Args: 

516 markup (str): A string containing markdown. 

517 code_theme (str, optional): Pygments theme for code blocks. Defaults to "monokai". See https://pygments.org/styles/ for code themes. 

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

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

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

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

522 enabled. Defaults to None. 

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

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

525 """ 

526 

527 elements: ClassVar[dict[str, type[MarkdownElement]]] = { 

528 "paragraph_open": Paragraph, 

529 "heading_open": Heading, 

530 "fence": CodeBlock, 

531 "code_block": CodeBlock, 

532 "blockquote_open": BlockQuote, 

533 "hr": HorizontalRule, 

534 "bullet_list_open": ListElement, 

535 "ordered_list_open": ListElement, 

536 "list_item_open": ListItem, 

537 "image": ImageItem, 

538 "table_open": TableElement, 

539 "tbody_open": TableBodyElement, 

540 "thead_open": TableHeaderElement, 

541 "tr_open": TableRowElement, 

542 "td_open": TableDataElement, 

543 "th_open": TableDataElement, 

544 } 

545 

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

547 

548 def __init__( 

549 self, 

550 markup: str, 

551 code_theme: str = "monokai", 

552 justify: JustifyMethod | None = None, 

553 style: str | Style = "none", 

554 hyperlinks: bool = True, 

555 inline_code_lexer: str | None = None, 

556 inline_code_theme: str | None = None, 

557 ) -> None: 

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

559 self.markup = markup 

560 self.parsed = parser.parse(markup) 

561 self.code_theme = code_theme 

562 self.justify: JustifyMethod | None = justify 

563 self.style = style 

564 self.hyperlinks = hyperlinks 

565 self.inline_code_lexer = inline_code_lexer 

566 self.inline_code_theme = inline_code_theme or code_theme 

567 

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

569 """Flattens the token stream.""" 

570 for token in tokens: 

571 is_fence = token.type == "fence" 

572 is_image = token.tag == "img" 

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

574 yield from self._flatten_tokens(token.children) 

575 else: 

576 yield token 

577 

578 def __rich_console__( 

579 self, console: Console, options: ConsoleOptions 

580 ) -> RenderResult: 

581 """Render markdown to the console.""" 

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

583 options = options.update(height=None) 

584 context = MarkdownContext( 

585 console, 

586 options, 

587 style, 

588 inline_code_lexer=self.inline_code_lexer, 

589 inline_code_theme=self.inline_code_theme, 

590 ) 

591 tokens = self.parsed 

592 inline_style_tags = self.inlines 

593 new_line = False 

594 _new_line_segment = Segment.line() 

595 

596 for token in self._flatten_tokens(tokens): 

597 node_type = token.type 

598 tag = token.tag 

599 

600 entering = token.nesting == 1 

601 exiting = token.nesting == -1 

602 self_closing = token.nesting == 0 

603 

604 if node_type == "text": 

605 context.on_text(token.content, node_type) 

606 elif node_type == "hardbreak": 

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

608 elif node_type == "softbreak": 

609 context.on_text(" ", node_type) 

610 elif node_type == "link_open": 

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

612 if self.hyperlinks: 

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

614 link_style += Style(link=href) 

615 context.enter_style(link_style) 

616 else: 

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

618 elif node_type == "link_close": 

619 if self.hyperlinks: 

620 context.leave_style() 

621 else: 

622 element = context.stack.pop() 

623 assert isinstance(element, Link) 

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

625 context.enter_style(link_style) 

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

627 context.leave_style() 

628 context.on_text(" (", node_type) 

629 link_url_style = console.get_style( 

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

631 ) 

632 context.enter_style(link_url_style) 

633 context.on_text(element.href, node_type) 

634 context.leave_style() 

635 context.on_text(")", node_type) 

636 elif ( 

637 tag in inline_style_tags 

638 and node_type != "fence" 

639 and node_type != "code_block" 

640 ): 

641 if entering: 

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

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

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

645 elif exiting: 

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

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

648 context.leave_style() 

649 else: 

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

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

652 if token.content: 

653 context.on_text(token.content, node_type) 

654 context.leave_style() 

655 else: 

656 # Map the markdown tag -> MarkdownElement renderable 

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

658 element = element_class.create(self, token) 

659 

660 if entering or self_closing: 

661 context.stack.push(element) 

662 element.on_enter(context) 

663 

664 if exiting: # CLOSING tag 

665 element = context.stack.pop() 

666 

667 should_render = not context.stack or ( 

668 context.stack 

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

670 ) 

671 

672 if should_render: 

673 if new_line: 

674 yield _new_line_segment 

675 

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

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

678 context.stack.pop() 

679 text = token.content 

680 if text is not None: 

681 element.on_text(context, text) 

682 

683 should_render = ( 

684 not context.stack 

685 or context.stack 

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

687 ) 

688 if should_render: 

689 if new_line and node_type != "inline": 

690 yield _new_line_segment 

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

692 

693 if exiting or self_closing: 

694 element.on_leave(context) 

695 new_line = element.new_line 

696 

697 

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

699 import argparse 

700 import sys 

701 

702 parser = argparse.ArgumentParser( 

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

704 ) 

705 parser.add_argument( 

706 "path", 

707 metavar="PATH", 

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

709 ) 

710 parser.add_argument( 

711 "-c", 

712 "--force-color", 

713 dest="force_color", 

714 action="store_true", 

715 default=None, 

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

717 ) 

718 parser.add_argument( 

719 "-t", 

720 "--code-theme", 

721 dest="code_theme", 

722 default="monokai", 

723 help="pygments code theme", 

724 ) 

725 parser.add_argument( 

726 "-i", 

727 "--inline-code-lexer", 

728 dest="inline_code_lexer", 

729 default=None, 

730 help="inline_code_lexer", 

731 ) 

732 parser.add_argument( 

733 "-y", 

734 "--hyperlinks", 

735 dest="hyperlinks", 

736 action="store_true", 

737 help="enable hyperlinks", 

738 ) 

739 parser.add_argument( 

740 "-w", 

741 "--width", 

742 type=int, 

743 dest="width", 

744 default=None, 

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

746 ) 

747 parser.add_argument( 

748 "-j", 

749 "--justify", 

750 dest="justify", 

751 action="store_true", 

752 help="enable full text justify", 

753 ) 

754 parser.add_argument( 

755 "-p", 

756 "--page", 

757 dest="page", 

758 action="store_true", 

759 help="use pager to scroll output", 

760 ) 

761 args = parser.parse_args() 

762 

763 from rich.console import Console 

764 

765 if args.path == "-": 

766 markdown_body = sys.stdin.read() 

767 else: 

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

769 markdown_body = markdown_file.read() 

770 

771 markdown = Markdown( 

772 markdown_body, 

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

774 code_theme=args.code_theme, 

775 hyperlinks=args.hyperlinks, 

776 inline_code_lexer=args.inline_code_lexer, 

777 ) 

778 if args.page: 

779 import io 

780 import pydoc 

781 

782 fileio = io.StringIO() 

783 console = Console( 

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

785 ) 

786 console.print(markdown) 

787 pydoc.pager(fileio.getvalue()) 

788 

789 else: 

790 console = Console( 

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

792 ) 

793 console.print(markdown)