markdown.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802
  1. from __future__ import annotations
  2. import sys
  3. from dataclasses import dataclass
  4. from typing import ClassVar, Iterable, get_args
  5. from markdown_it import MarkdownIt
  6. from markdown_it.token import Token
  7. from rich.table import Table
  8. from . import box
  9. from ._loop import loop_first
  10. from ._stack import Stack
  11. from .console import Console, ConsoleOptions, JustifyMethod, RenderResult
  12. from .containers import Renderables
  13. from .jupyter import JupyterMixin
  14. from .rule import Rule
  15. from .segment import Segment
  16. from .style import Style, StyleStack
  17. from .syntax import Syntax
  18. from .text import Text, TextType
  19. class MarkdownElement:
  20. new_line: ClassVar[bool] = True
  21. @classmethod
  22. def create(cls, markdown: Markdown, token: Token) -> MarkdownElement:
  23. """Factory to create markdown element,
  24. Args:
  25. markdown (Markdown): The parent Markdown object.
  26. token (Token): A node from markdown-it.
  27. Returns:
  28. MarkdownElement: A new markdown element
  29. """
  30. return cls()
  31. def on_enter(self, context: MarkdownContext) -> None:
  32. """Called when the node is entered.
  33. Args:
  34. context (MarkdownContext): The markdown context.
  35. """
  36. def on_text(self, context: MarkdownContext, text: TextType) -> None:
  37. """Called when text is parsed.
  38. Args:
  39. context (MarkdownContext): The markdown context.
  40. """
  41. def on_leave(self, context: MarkdownContext) -> None:
  42. """Called when the parser leaves the element.
  43. Args:
  44. context (MarkdownContext): [description]
  45. """
  46. def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
  47. """Called when a child element is closed.
  48. This method allows a parent element to take over rendering of its children.
  49. Args:
  50. context (MarkdownContext): The markdown context.
  51. child (MarkdownElement): The child markdown element.
  52. Returns:
  53. bool: Return True to render the element, or False to not render the element.
  54. """
  55. return True
  56. def __rich_console__(
  57. self, console: Console, options: ConsoleOptions
  58. ) -> RenderResult:
  59. return ()
  60. class UnknownElement(MarkdownElement):
  61. """An unknown element.
  62. Hopefully there will be no unknown elements, and we will have a MarkdownElement for
  63. everything in the document.
  64. """
  65. class TextElement(MarkdownElement):
  66. """Base class for elements that render text."""
  67. style_name = "none"
  68. def on_enter(self, context: MarkdownContext) -> None:
  69. self.style = context.enter_style(self.style_name)
  70. self.text = Text(justify="left")
  71. def on_text(self, context: MarkdownContext, text: TextType) -> None:
  72. self.text.append(text, context.current_style if isinstance(text, str) else None)
  73. def on_leave(self, context: MarkdownContext) -> None:
  74. context.leave_style()
  75. class Paragraph(TextElement):
  76. """A Paragraph."""
  77. style_name = "markdown.paragraph"
  78. justify: JustifyMethod
  79. @classmethod
  80. def create(cls, markdown: Markdown, token: Token) -> Paragraph:
  81. return cls(justify=markdown.justify or "left")
  82. def __init__(self, justify: JustifyMethod) -> None:
  83. self.justify = justify
  84. def __rich_console__(
  85. self, console: Console, options: ConsoleOptions
  86. ) -> RenderResult:
  87. self.text.justify = self.justify
  88. yield self.text
  89. @dataclass
  90. class HeadingFormat:
  91. justify: JustifyMethod = "left"
  92. style: str = ""
  93. class Heading(TextElement):
  94. """A heading."""
  95. LEVEL_ALIGN: ClassVar[dict[str, JustifyMethod]] = {
  96. "h1": "center",
  97. "h2": "left",
  98. "h3": "left",
  99. "h4": "left",
  100. "h5": "left",
  101. "h6": "left",
  102. }
  103. @classmethod
  104. def create(cls, markdown: Markdown, token: Token) -> Heading:
  105. return cls(token.tag)
  106. def on_enter(self, context: MarkdownContext) -> None:
  107. self.text = Text()
  108. context.enter_style(self.style_name)
  109. def __init__(self, tag: str) -> None:
  110. self.tag = tag
  111. self.style_name = f"markdown.{tag}"
  112. super().__init__()
  113. def __rich_console__(
  114. self, console: Console, options: ConsoleOptions
  115. ) -> RenderResult:
  116. text = self.text.copy()
  117. heading_justify = self.LEVEL_ALIGN.get(self.tag, "left")
  118. text.justify = heading_justify
  119. yield text
  120. class CodeBlock(TextElement):
  121. """A code block with syntax highlighting."""
  122. style_name = "markdown.code_block"
  123. @classmethod
  124. def create(cls, markdown: Markdown, token: Token) -> CodeBlock:
  125. node_info = token.info or ""
  126. lexer_name = node_info.partition(" ")[0]
  127. return cls(lexer_name or "text", markdown.code_theme)
  128. def __init__(self, lexer_name: str, theme: str) -> None:
  129. self.lexer_name = lexer_name
  130. self.theme = theme
  131. def __rich_console__(
  132. self, console: Console, options: ConsoleOptions
  133. ) -> RenderResult:
  134. code = str(self.text).rstrip()
  135. syntax = Syntax(
  136. code, self.lexer_name, theme=self.theme, word_wrap=True, padding=1
  137. )
  138. yield syntax
  139. class BlockQuote(TextElement):
  140. """A block quote."""
  141. style_name = "markdown.block_quote"
  142. def __init__(self) -> None:
  143. self.elements: Renderables = Renderables()
  144. def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
  145. self.elements.append(child)
  146. return False
  147. def __rich_console__(
  148. self, console: Console, options: ConsoleOptions
  149. ) -> RenderResult:
  150. render_options = options.update(width=options.max_width - 4)
  151. lines = console.render_lines(self.elements, render_options, style=self.style)
  152. style = self.style
  153. new_line = Segment("\n")
  154. padding = Segment("▌ ", style)
  155. for line in lines:
  156. yield padding
  157. yield from line
  158. yield new_line
  159. class HorizontalRule(MarkdownElement):
  160. """A horizontal rule to divide sections."""
  161. new_line = False
  162. def __rich_console__(
  163. self, console: Console, options: ConsoleOptions
  164. ) -> RenderResult:
  165. style = console.get_style("markdown.hr", default="none")
  166. yield Rule(style=style, characters="-")
  167. yield Text()
  168. class TableElement(MarkdownElement):
  169. """MarkdownElement corresponding to `table_open`."""
  170. def __init__(self) -> None:
  171. self.header: TableHeaderElement | None = None
  172. self.body: TableBodyElement | None = None
  173. def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
  174. if isinstance(child, TableHeaderElement):
  175. self.header = child
  176. elif isinstance(child, TableBodyElement):
  177. self.body = child
  178. else:
  179. raise RuntimeError("Couldn't process markdown table.")
  180. return False
  181. def __rich_console__(
  182. self, console: Console, options: ConsoleOptions
  183. ) -> RenderResult:
  184. table = Table(
  185. box=box.SIMPLE,
  186. pad_edge=False,
  187. style="markdown.table.border",
  188. show_edge=True,
  189. collapse_padding=True,
  190. )
  191. if self.header is not None and self.header.row is not None:
  192. for column in self.header.row.cells:
  193. heading = column.content.copy()
  194. heading.stylize("markdown.table.header")
  195. table.add_column(heading)
  196. if self.body is not None:
  197. for row in self.body.rows:
  198. row_content = [element.content for element in row.cells]
  199. table.add_row(*row_content)
  200. yield table
  201. class TableHeaderElement(MarkdownElement):
  202. """MarkdownElement corresponding to `thead_open` and `thead_close`."""
  203. def __init__(self) -> None:
  204. self.row: TableRowElement | None = None
  205. def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
  206. assert isinstance(child, TableRowElement)
  207. self.row = child
  208. return False
  209. class TableBodyElement(MarkdownElement):
  210. """MarkdownElement corresponding to `tbody_open` and `tbody_close`."""
  211. def __init__(self) -> None:
  212. self.rows: list[TableRowElement] = []
  213. def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
  214. assert isinstance(child, TableRowElement)
  215. self.rows.append(child)
  216. return False
  217. class TableRowElement(MarkdownElement):
  218. """MarkdownElement corresponding to `tr_open` and `tr_close`."""
  219. def __init__(self) -> None:
  220. self.cells: list[TableDataElement] = []
  221. def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
  222. assert isinstance(child, TableDataElement)
  223. self.cells.append(child)
  224. return False
  225. class TableDataElement(MarkdownElement):
  226. """MarkdownElement corresponding to `td_open` and `td_close`
  227. and `th_open` and `th_close`."""
  228. @classmethod
  229. def create(cls, markdown: Markdown, token: Token) -> MarkdownElement:
  230. style = str(token.attrs.get("style")) or ""
  231. justify: JustifyMethod
  232. if "text-align:right" in style:
  233. justify = "right"
  234. elif "text-align:center" in style:
  235. justify = "center"
  236. elif "text-align:left" in style:
  237. justify = "left"
  238. else:
  239. justify = "default"
  240. assert justify in get_args(JustifyMethod)
  241. return cls(justify=justify)
  242. def __init__(self, justify: JustifyMethod) -> None:
  243. self.content: Text = Text("", justify=justify)
  244. self.justify = justify
  245. def on_text(self, context: MarkdownContext, text: TextType) -> None:
  246. if isinstance(text, str):
  247. self.content.append(text, context.current_style)
  248. else:
  249. self.content.append_text(text)
  250. class ListElement(MarkdownElement):
  251. """A list element."""
  252. @classmethod
  253. def create(cls, markdown: Markdown, token: Token) -> ListElement:
  254. return cls(token.type, int(token.attrs.get("start", 1)))
  255. def __init__(self, list_type: str, list_start: int | None) -> None:
  256. self.items: list[ListItem] = []
  257. self.list_type = list_type
  258. self.list_start = list_start
  259. def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
  260. assert isinstance(child, ListItem)
  261. self.items.append(child)
  262. return False
  263. def __rich_console__(
  264. self, console: Console, options: ConsoleOptions
  265. ) -> RenderResult:
  266. if self.list_type == "bullet_list_open":
  267. for item in self.items:
  268. yield from item.render_bullet(console, options)
  269. else:
  270. number = 1 if self.list_start is None else self.list_start
  271. last_number = number + len(self.items)
  272. for index, item in enumerate(self.items):
  273. yield from item.render_number(
  274. console, options, number + index, last_number
  275. )
  276. class ListItem(TextElement):
  277. """An item in a list."""
  278. style_name = "markdown.item"
  279. def __init__(self) -> None:
  280. self.elements: Renderables = Renderables()
  281. def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
  282. self.elements.append(child)
  283. return False
  284. def render_bullet(self, console: Console, options: ConsoleOptions) -> RenderResult:
  285. render_options = options.update(width=options.max_width - 3)
  286. lines = console.render_lines(self.elements, render_options, style=self.style)
  287. bullet_style = console.get_style("markdown.item.bullet", default="none")
  288. bullet = Segment(" • ", bullet_style)
  289. padding = Segment(" " * 3, bullet_style)
  290. new_line = Segment("\n")
  291. for first, line in loop_first(lines):
  292. yield bullet if first else padding
  293. yield from line
  294. yield new_line
  295. def render_number(
  296. self, console: Console, options: ConsoleOptions, number: int, last_number: int
  297. ) -> RenderResult:
  298. number_width = len(str(last_number)) + 2
  299. render_options = options.update(width=options.max_width - number_width)
  300. lines = console.render_lines(self.elements, render_options, style=self.style)
  301. number_style = console.get_style("markdown.item.number", default="none")
  302. new_line = Segment("\n")
  303. padding = Segment(" " * number_width, number_style)
  304. numeral = Segment(f"{number}".rjust(number_width - 1) + " ", number_style)
  305. for first, line in loop_first(lines):
  306. yield numeral if first else padding
  307. yield from line
  308. yield new_line
  309. class Link(TextElement):
  310. @classmethod
  311. def create(cls, markdown: Markdown, token: Token) -> MarkdownElement:
  312. url = token.attrs.get("href", "#")
  313. return cls(token.content, str(url))
  314. def __init__(self, text: str, href: str):
  315. self.text = Text(text)
  316. self.href = href
  317. class ImageItem(TextElement):
  318. """Renders a placeholder for an image."""
  319. new_line = False
  320. @classmethod
  321. def create(cls, markdown: Markdown, token: Token) -> MarkdownElement:
  322. """Factory to create markdown element,
  323. Args:
  324. markdown (Markdown): The parent Markdown object.
  325. token (Any): A token from markdown-it.
  326. Returns:
  327. MarkdownElement: A new markdown element
  328. """
  329. return cls(str(token.attrs.get("src", "")), markdown.hyperlinks)
  330. def __init__(self, destination: str, hyperlinks: bool) -> None:
  331. self.destination = destination
  332. self.hyperlinks = hyperlinks
  333. self.link: str | None = None
  334. super().__init__()
  335. def on_enter(self, context: MarkdownContext) -> None:
  336. self.link = context.current_style.link
  337. self.text = Text(justify="left")
  338. super().on_enter(context)
  339. def __rich_console__(
  340. self, console: Console, options: ConsoleOptions
  341. ) -> RenderResult:
  342. link_style = Style(link=self.link or self.destination or None)
  343. title = self.text or Text(self.destination.strip("/").rsplit("/", 1)[-1])
  344. if self.hyperlinks:
  345. title.stylize(link_style)
  346. text = Text.assemble("🌆 ", title, " ", end="")
  347. yield text
  348. class MarkdownContext:
  349. """Manages the console render state."""
  350. def __init__(
  351. self,
  352. console: Console,
  353. options: ConsoleOptions,
  354. style: Style,
  355. inline_code_lexer: str | None = None,
  356. inline_code_theme: str = "monokai",
  357. ) -> None:
  358. self.console = console
  359. self.options = options
  360. self.style_stack: StyleStack = StyleStack(style)
  361. self.stack: Stack[MarkdownElement] = Stack()
  362. self._syntax: Syntax | None = None
  363. if inline_code_lexer is not None:
  364. self._syntax = Syntax("", inline_code_lexer, theme=inline_code_theme)
  365. @property
  366. def current_style(self) -> Style:
  367. """Current style which is the product of all styles on the stack."""
  368. return self.style_stack.current
  369. def on_text(self, text: str, node_type: str) -> None:
  370. """Called when the parser visits text."""
  371. if node_type in {"fence", "code_inline"} and self._syntax is not None:
  372. highlight_text = self._syntax.highlight(text)
  373. highlight_text.rstrip()
  374. self.stack.top.on_text(
  375. self, Text.assemble(highlight_text, style=self.style_stack.current)
  376. )
  377. else:
  378. self.stack.top.on_text(self, text)
  379. def enter_style(self, style_name: str | Style) -> Style:
  380. """Enter a style context."""
  381. style = self.console.get_style(style_name, default="none")
  382. self.style_stack.push(style)
  383. return self.current_style
  384. def leave_style(self) -> Style:
  385. """Leave a style context."""
  386. style = self.style_stack.pop()
  387. return style
  388. class Markdown(JupyterMixin):
  389. """A Markdown renderable.
  390. Args:
  391. markup (str): A string containing markdown.
  392. code_theme (str, optional): Pygments theme for code blocks. Defaults to "monokai". See https://pygments.org/styles/ for code themes.
  393. justify (JustifyMethod, optional): Justify value for paragraphs. Defaults to None.
  394. style (Union[str, Style], optional): Optional style to apply to markdown.
  395. hyperlinks (bool, optional): Enable hyperlinks. Defaults to ``True``.
  396. inline_code_lexer: (str, optional): Lexer to use if inline code highlighting is
  397. enabled. Defaults to None.
  398. inline_code_theme: (Optional[str], optional): Pygments theme for inline code
  399. highlighting, or None for no highlighting. Defaults to None.
  400. """
  401. elements: ClassVar[dict[str, type[MarkdownElement]]] = {
  402. "paragraph_open": Paragraph,
  403. "heading_open": Heading,
  404. "fence": CodeBlock,
  405. "code_block": CodeBlock,
  406. "blockquote_open": BlockQuote,
  407. "hr": HorizontalRule,
  408. "bullet_list_open": ListElement,
  409. "ordered_list_open": ListElement,
  410. "list_item_open": ListItem,
  411. "image": ImageItem,
  412. "table_open": TableElement,
  413. "tbody_open": TableBodyElement,
  414. "thead_open": TableHeaderElement,
  415. "tr_open": TableRowElement,
  416. "td_open": TableDataElement,
  417. "th_open": TableDataElement,
  418. }
  419. inlines = {"em", "strong", "code", "s"}
  420. def __init__(
  421. self,
  422. markup: str,
  423. code_theme: str = "monokai",
  424. justify: JustifyMethod | None = None,
  425. style: str | Style = "none",
  426. hyperlinks: bool = True,
  427. inline_code_lexer: str | None = None,
  428. inline_code_theme: str | None = None,
  429. ) -> None:
  430. parser = MarkdownIt().enable("strikethrough").enable("table")
  431. self.markup = markup
  432. self.parsed = parser.parse(markup)
  433. self.code_theme = code_theme
  434. self.justify: JustifyMethod | None = justify
  435. self.style = style
  436. self.hyperlinks = hyperlinks
  437. self.inline_code_lexer = inline_code_lexer
  438. self.inline_code_theme = inline_code_theme or code_theme
  439. def _flatten_tokens(self, tokens: Iterable[Token]) -> Iterable[Token]:
  440. """Flattens the token stream."""
  441. for token in tokens:
  442. is_fence = token.type == "fence"
  443. is_image = token.tag == "img"
  444. if token.children and not (is_image or is_fence):
  445. yield from self._flatten_tokens(token.children)
  446. else:
  447. yield token
  448. def __rich_console__(
  449. self, console: Console, options: ConsoleOptions
  450. ) -> RenderResult:
  451. """Render markdown to the console."""
  452. style = console.get_style(self.style, default="none")
  453. options = options.update(height=None)
  454. context = MarkdownContext(
  455. console,
  456. options,
  457. style,
  458. inline_code_lexer=self.inline_code_lexer,
  459. inline_code_theme=self.inline_code_theme,
  460. )
  461. tokens = self.parsed
  462. inline_style_tags = self.inlines
  463. new_line = False
  464. _new_line_segment = Segment.line()
  465. for token in self._flatten_tokens(tokens):
  466. node_type = token.type
  467. tag = token.tag
  468. entering = token.nesting == 1
  469. exiting = token.nesting == -1
  470. self_closing = token.nesting == 0
  471. if node_type == "text":
  472. context.on_text(token.content, node_type)
  473. elif node_type == "hardbreak":
  474. context.on_text("\n", node_type)
  475. elif node_type == "softbreak":
  476. context.on_text(" ", node_type)
  477. elif node_type == "link_open":
  478. href = str(token.attrs.get("href", ""))
  479. if self.hyperlinks:
  480. link_style = console.get_style("markdown.link_url", default="none")
  481. link_style += Style(link=href)
  482. context.enter_style(link_style)
  483. else:
  484. context.stack.push(Link.create(self, token))
  485. elif node_type == "html_inline":
  486. if token.content == "<kbd>":
  487. kbd_style = console.get_style("markdown.kbd", default="bold")
  488. context.enter_style(kbd_style)
  489. elif token.content == "</kbd>":
  490. context.leave_style()
  491. else:
  492. continue
  493. elif node_type == "link_close":
  494. if self.hyperlinks:
  495. context.leave_style()
  496. else:
  497. element = context.stack.pop()
  498. assert isinstance(element, Link)
  499. link_style = console.get_style("markdown.link", default="none")
  500. context.enter_style(link_style)
  501. context.on_text(element.text.plain, node_type)
  502. context.leave_style()
  503. context.on_text(" (", node_type)
  504. link_url_style = console.get_style(
  505. "markdown.link_url", default="none"
  506. )
  507. context.enter_style(link_url_style)
  508. context.on_text(element.href, node_type)
  509. context.leave_style()
  510. context.on_text(")", node_type)
  511. elif (
  512. tag in inline_style_tags
  513. and node_type != "fence"
  514. and node_type != "code_block"
  515. ):
  516. if entering:
  517. # If it's an opening inline token e.g. strong, em, etc.
  518. # Then we move into a style context i.e. push to stack.
  519. context.enter_style(f"markdown.{tag}")
  520. elif exiting:
  521. # If it's a closing inline style, then we pop the style
  522. # off of the stack, to move out of the context of it...
  523. context.leave_style()
  524. else:
  525. # If it's a self-closing inline style e.g. `code_inline`
  526. context.enter_style(f"markdown.{tag}")
  527. if token.content:
  528. context.on_text(token.content, node_type)
  529. context.leave_style()
  530. else:
  531. # Map the markdown tag -> MarkdownElement renderable
  532. element_class = self.elements.get(token.type) or UnknownElement
  533. element = element_class.create(self, token)
  534. if entering or self_closing:
  535. context.stack.push(element)
  536. element.on_enter(context)
  537. if exiting: # CLOSING tag
  538. element = context.stack.pop()
  539. should_render = not context.stack or (
  540. context.stack
  541. and context.stack.top.on_child_close(context, element)
  542. )
  543. if should_render:
  544. if new_line:
  545. yield _new_line_segment
  546. yield from console.render(element, context.options)
  547. elif self_closing: # SELF-CLOSING tags (e.g. text, code, image)
  548. context.stack.pop()
  549. text = token.content
  550. if text is not None:
  551. element.on_text(context, text)
  552. should_render = (
  553. not context.stack
  554. or context.stack
  555. and context.stack.top.on_child_close(context, element)
  556. )
  557. if should_render:
  558. if new_line and node_type != "inline":
  559. yield _new_line_segment
  560. yield from console.render(element, context.options)
  561. if exiting or self_closing:
  562. element.on_leave(context)
  563. new_line = element.new_line
  564. if __name__ == "__main__": # pragma: no cover
  565. import argparse
  566. import sys
  567. parser = argparse.ArgumentParser(
  568. description="Render Markdown to the console with Rich"
  569. )
  570. parser.add_argument(
  571. "path",
  572. metavar="PATH",
  573. help="path to markdown file, or - for stdin",
  574. )
  575. parser.add_argument(
  576. "-c",
  577. "--force-color",
  578. dest="force_color",
  579. action="store_true",
  580. default=None,
  581. help="force color for non-terminals",
  582. )
  583. parser.add_argument(
  584. "-t",
  585. "--code-theme",
  586. dest="code_theme",
  587. default="monokai",
  588. help="pygments code theme",
  589. )
  590. parser.add_argument(
  591. "-i",
  592. "--inline-code-lexer",
  593. dest="inline_code_lexer",
  594. default=None,
  595. help="inline_code_lexer",
  596. )
  597. parser.add_argument(
  598. "-y",
  599. "--hyperlinks",
  600. dest="hyperlinks",
  601. action="store_true",
  602. help="enable hyperlinks",
  603. )
  604. parser.add_argument(
  605. "-w",
  606. "--width",
  607. type=int,
  608. dest="width",
  609. default=None,
  610. help="width of output (default will auto-detect)",
  611. )
  612. parser.add_argument(
  613. "-j",
  614. "--justify",
  615. dest="justify",
  616. action="store_true",
  617. help="enable full text justify",
  618. )
  619. parser.add_argument(
  620. "-p",
  621. "--page",
  622. dest="page",
  623. action="store_true",
  624. help="use pager to scroll output",
  625. )
  626. args = parser.parse_args()
  627. from rich.console import Console
  628. if args.path == "-":
  629. markdown_body = sys.stdin.read()
  630. else:
  631. with open(args.path, encoding="utf-8") as markdown_file:
  632. markdown_body = markdown_file.read()
  633. markdown = Markdown(
  634. markdown_body,
  635. justify="full" if args.justify else "left",
  636. code_theme=args.code_theme,
  637. hyperlinks=args.hyperlinks,
  638. inline_code_lexer=args.inline_code_lexer,
  639. )
  640. if args.page:
  641. import io
  642. import pydoc
  643. fileio = io.StringIO()
  644. console = Console(
  645. file=fileio, force_terminal=args.force_color, width=args.width
  646. )
  647. console.print(markdown)
  648. pydoc.pager(fileio.getvalue())
  649. else:
  650. console = Console(
  651. force_terminal=args.force_color, width=args.width, record=True
  652. )
  653. console.print(markdown)