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

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

380 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 if isinstance(text, str): 

335 self.content.append(text, context.current_style) 

336 else: 

337 self.content.append_text(text) 

338 

339 

340class ListElement(MarkdownElement): 

341 """A list element.""" 

342 

343 @classmethod 

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

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

346 

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

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

349 self.list_type = list_type 

350 self.list_start = list_start 

351 

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

353 assert isinstance(child, ListItem) 

354 self.items.append(child) 

355 return False 

356 

357 def __rich_console__( 

358 self, console: Console, options: ConsoleOptions 

359 ) -> RenderResult: 

360 if self.list_type == "bullet_list_open": 

361 for item in self.items: 

362 yield from item.render_bullet(console, options) 

363 else: 

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

365 last_number = number + len(self.items) 

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

367 yield from item.render_number( 

368 console, options, number + index, last_number 

369 ) 

370 

371 

372class ListItem(TextElement): 

373 """An item in a list.""" 

374 

375 style_name = "markdown.item" 

376 

377 def __init__(self) -> None: 

378 self.elements: Renderables = Renderables() 

379 

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

381 self.elements.append(child) 

382 return False 

383 

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

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

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

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

388 

389 bullet = Segment(" • ", bullet_style) 

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

391 new_line = Segment("\n") 

392 for first, line in loop_first(lines): 

393 yield bullet if first else padding 

394 yield from line 

395 yield new_line 

396 

397 def render_number( 

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

399 ) -> RenderResult: 

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

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

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

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

404 

405 new_line = Segment("\n") 

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

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

408 for first, line in loop_first(lines): 

409 yield numeral if first else padding 

410 yield from line 

411 yield new_line 

412 

413 

414class Link(TextElement): 

415 @classmethod 

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

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

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

419 

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

421 self.text = Text(text) 

422 self.href = href 

423 

424 

425class ImageItem(TextElement): 

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

427 

428 new_line = False 

429 

430 @classmethod 

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

432 """Factory to create markdown element, 

433 

434 Args: 

435 markdown (Markdown): The parent Markdown object. 

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

437 

438 Returns: 

439 MarkdownElement: A new markdown element 

440 """ 

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

442 

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

444 self.destination = destination 

445 self.hyperlinks = hyperlinks 

446 self.link: str | None = None 

447 super().__init__() 

448 

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

450 self.link = context.current_style.link 

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

452 super().on_enter(context) 

453 

454 def __rich_console__( 

455 self, console: Console, options: ConsoleOptions 

456 ) -> RenderResult: 

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

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

459 if self.hyperlinks: 

460 title.stylize(link_style) 

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

462 yield text 

463 

464 

465class MarkdownContext: 

466 """Manages the console render state.""" 

467 

468 def __init__( 

469 self, 

470 console: Console, 

471 options: ConsoleOptions, 

472 style: Style, 

473 inline_code_lexer: str | None = None, 

474 inline_code_theme: str = "monokai", 

475 ) -> None: 

476 self.console = console 

477 self.options = options 

478 self.style_stack: StyleStack = StyleStack(style) 

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

480 

481 self._syntax: Syntax | None = None 

482 if inline_code_lexer is not None: 

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

484 

485 @property 

486 def current_style(self) -> Style: 

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

488 return self.style_stack.current 

489 

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

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

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

493 highlight_text = self._syntax.highlight(text) 

494 highlight_text.rstrip() 

495 self.stack.top.on_text( 

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

497 ) 

498 else: 

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

500 

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

502 """Enter a style context.""" 

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

504 self.style_stack.push(style) 

505 return self.current_style 

506 

507 def leave_style(self) -> Style: 

508 """Leave a style context.""" 

509 style = self.style_stack.pop() 

510 return style 

511 

512 

513class Markdown(JupyterMixin): 

514 """A Markdown renderable. 

515 

516 Args: 

517 markup (str): A string containing markdown. 

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

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

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

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

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

523 enabled. Defaults to None. 

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

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

526 """ 

527 

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

529 "paragraph_open": Paragraph, 

530 "heading_open": Heading, 

531 "fence": CodeBlock, 

532 "code_block": CodeBlock, 

533 "blockquote_open": BlockQuote, 

534 "hr": HorizontalRule, 

535 "bullet_list_open": ListElement, 

536 "ordered_list_open": ListElement, 

537 "list_item_open": ListItem, 

538 "image": ImageItem, 

539 "table_open": TableElement, 

540 "tbody_open": TableBodyElement, 

541 "thead_open": TableHeaderElement, 

542 "tr_open": TableRowElement, 

543 "td_open": TableDataElement, 

544 "th_open": TableDataElement, 

545 } 

546 

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

548 

549 def __init__( 

550 self, 

551 markup: str, 

552 code_theme: str = "monokai", 

553 justify: JustifyMethod | None = None, 

554 style: str | Style = "none", 

555 hyperlinks: bool = True, 

556 inline_code_lexer: str | None = None, 

557 inline_code_theme: str | None = None, 

558 ) -> None: 

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

560 self.markup = markup 

561 self.parsed = parser.parse(markup) 

562 self.code_theme = code_theme 

563 self.justify: JustifyMethod | None = justify 

564 self.style = style 

565 self.hyperlinks = hyperlinks 

566 self.inline_code_lexer = inline_code_lexer 

567 self.inline_code_theme = inline_code_theme or code_theme 

568 

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

570 """Flattens the token stream.""" 

571 for token in tokens: 

572 is_fence = token.type == "fence" 

573 is_image = token.tag == "img" 

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

575 yield from self._flatten_tokens(token.children) 

576 else: 

577 yield token 

578 

579 def __rich_console__( 

580 self, console: Console, options: ConsoleOptions 

581 ) -> RenderResult: 

582 """Render markdown to the console.""" 

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

584 options = options.update(height=None) 

585 context = MarkdownContext( 

586 console, 

587 options, 

588 style, 

589 inline_code_lexer=self.inline_code_lexer, 

590 inline_code_theme=self.inline_code_theme, 

591 ) 

592 tokens = self.parsed 

593 inline_style_tags = self.inlines 

594 new_line = False 

595 _new_line_segment = Segment.line() 

596 

597 for token in self._flatten_tokens(tokens): 

598 node_type = token.type 

599 tag = token.tag 

600 

601 entering = token.nesting == 1 

602 exiting = token.nesting == -1 

603 self_closing = token.nesting == 0 

604 

605 if node_type == "text": 

606 context.on_text(token.content, node_type) 

607 elif node_type == "hardbreak": 

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

609 elif node_type == "softbreak": 

610 context.on_text(" ", node_type) 

611 elif node_type == "link_open": 

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

613 if self.hyperlinks: 

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

615 link_style += Style(link=href) 

616 context.enter_style(link_style) 

617 else: 

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

619 elif node_type == "html_inline": 

620 if token.content == "<kbd>": 

621 kbd_style = console.get_style("markdown.kbd", default="bold") 

622 context.enter_style(kbd_style) 

623 elif token.content == "</kbd>": 

624 context.leave_style() 

625 else: 

626 continue 

627 elif node_type == "link_close": 

628 if self.hyperlinks: 

629 context.leave_style() 

630 else: 

631 element = context.stack.pop() 

632 assert isinstance(element, Link) 

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

634 context.enter_style(link_style) 

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

636 context.leave_style() 

637 context.on_text(" (", node_type) 

638 link_url_style = console.get_style( 

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

640 ) 

641 context.enter_style(link_url_style) 

642 context.on_text(element.href, node_type) 

643 context.leave_style() 

644 context.on_text(")", node_type) 

645 elif ( 

646 tag in inline_style_tags 

647 and node_type != "fence" 

648 and node_type != "code_block" 

649 ): 

650 if entering: 

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

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

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

654 elif exiting: 

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

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

657 context.leave_style() 

658 else: 

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

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

661 if token.content: 

662 context.on_text(token.content, node_type) 

663 context.leave_style() 

664 else: 

665 # Map the markdown tag -> MarkdownElement renderable 

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

667 element = element_class.create(self, token) 

668 

669 if entering or self_closing: 

670 context.stack.push(element) 

671 element.on_enter(context) 

672 

673 if exiting: # CLOSING tag 

674 element = context.stack.pop() 

675 

676 should_render = not context.stack or ( 

677 context.stack 

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

679 ) 

680 

681 if should_render: 

682 if new_line: 

683 yield _new_line_segment 

684 

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

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

687 context.stack.pop() 

688 text = token.content 

689 if text is not None: 

690 element.on_text(context, text) 

691 

692 should_render = ( 

693 not context.stack 

694 or context.stack 

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

696 ) 

697 if should_render: 

698 if new_line and node_type != "inline": 

699 yield _new_line_segment 

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

701 

702 if exiting or self_closing: 

703 element.on_leave(context) 

704 new_line = element.new_line 

705 

706 

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

708 import argparse 

709 import sys 

710 

711 parser = argparse.ArgumentParser( 

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

713 ) 

714 parser.add_argument( 

715 "path", 

716 metavar="PATH", 

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

718 ) 

719 parser.add_argument( 

720 "-c", 

721 "--force-color", 

722 dest="force_color", 

723 action="store_true", 

724 default=None, 

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

726 ) 

727 parser.add_argument( 

728 "-t", 

729 "--code-theme", 

730 dest="code_theme", 

731 default="monokai", 

732 help="pygments code theme", 

733 ) 

734 parser.add_argument( 

735 "-i", 

736 "--inline-code-lexer", 

737 dest="inline_code_lexer", 

738 default=None, 

739 help="inline_code_lexer", 

740 ) 

741 parser.add_argument( 

742 "-y", 

743 "--hyperlinks", 

744 dest="hyperlinks", 

745 action="store_true", 

746 help="enable hyperlinks", 

747 ) 

748 parser.add_argument( 

749 "-w", 

750 "--width", 

751 type=int, 

752 dest="width", 

753 default=None, 

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

755 ) 

756 parser.add_argument( 

757 "-j", 

758 "--justify", 

759 dest="justify", 

760 action="store_true", 

761 help="enable full text justify", 

762 ) 

763 parser.add_argument( 

764 "-p", 

765 "--page", 

766 dest="page", 

767 action="store_true", 

768 help="use pager to scroll output", 

769 ) 

770 args = parser.parse_args() 

771 

772 from rich.console import Console 

773 

774 if args.path == "-": 

775 markdown_body = sys.stdin.read() 

776 else: 

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

778 markdown_body = markdown_file.read() 

779 

780 markdown = Markdown( 

781 markdown_body, 

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

783 code_theme=args.code_theme, 

784 hyperlinks=args.hyperlinks, 

785 inline_code_lexer=args.inline_code_lexer, 

786 ) 

787 if args.page: 

788 import io 

789 import pydoc 

790 

791 fileio = io.StringIO() 

792 console = Console( 

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

794 ) 

795 console.print(markdown) 

796 pydoc.pager(fileio.getvalue()) 

797 

798 else: 

799 console = Console( 

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

801 ) 

802 console.print(markdown)