Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
markdown.py801 linesDownload Raw Back to rich
1from __future__ import annotations2 3import sys4from typing import ClassVar, Dict, Iterable, List, Optional, Type, Union5 6from markdown_it import MarkdownIt7from markdown_it.token import Token8 9if sys.version_info >= (3, 8):10    from typing import get_args11else:12    from typing_extensions import get_args  # pragma: no cover13 14from rich.table import Table15 16from . import box17from ._loop import loop_first18from ._stack import Stack19from .console import Console, ConsoleOptions, JustifyMethod, RenderResult20from .containers import Renderables21from .jupyter import JupyterMixin22from .panel import Panel23from .rule import Rule24from .segment import Segment25from .style import Style, StyleStack26from .syntax import Syntax27from .text import Text, TextType28 29 30class MarkdownElement:31    new_line: ClassVar[bool] = True32 33    @classmethod34    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 element43        """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 True82 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 for93    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: JustifyMethod119 120    @classmethod121    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 = justify126 127    def __rich_console__(128        self, console: Console, options: ConsoleOptions129    ) -> RenderResult:130        self.text.justify = self.justify131        yield self.text132 133 134class Heading(TextElement):135    """A heading."""136 137    @classmethod138    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 = tag147        self.style_name = f"markdown.{tag}"148        super().__init__()149 150    def __rich_console__(151        self, console: Console, options: ConsoleOptions152    ) -> RenderResult:153        text = self.text154        text.justify = "center"155        if self.tag == "h1":156            # Draw a border around h1s157            yield Panel(158                text,159                box=box.HEAVY,160                style="markdown.h1.border",161            )162        else:163            # Styled text for h2 and beyond164            if self.tag == "h2":165                yield Text("")166            yield text167 168 169class CodeBlock(TextElement):170    """A code block with syntax highlighting."""171 172    style_name = "markdown.code_block"173 174    @classmethod175    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 "text", markdown.code_theme)179 180    def __init__(self, lexer_name: str, theme: str) -> None:181        self.lexer_name = lexer_name182        self.theme = theme183 184    def __rich_console__(185        self, console: Console, options: ConsoleOptions186    ) -> RenderResult:187        code = str(self.text).rstrip()188        syntax = Syntax(189            code, self.lexer_name, theme=self.theme, word_wrap=True, padding=1190        )191        yield syntax192 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 False207 208    def __rich_console__(209        self, console: Console, options: ConsoleOptions210    ) -> 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.style214        new_line = Segment("\n")215        padding = Segment("▌ ", style)216        for line in lines:217            yield padding218            yield from line219            yield new_line220 221 222class HorizontalRule(MarkdownElement):223    """A horizontal rule to divide sections."""224 225    new_line = False226 227    def __rich_console__(228        self, console: Console, options: ConsoleOptions229    ) -> 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 = None239        self.body: TableBodyElement | None = None240 241    def on_child_close(242        self, context: "MarkdownContext", child: "MarkdownElement"243    ) -> bool:244        if isinstance(child, TableHeaderElement):245            self.header = child246        elif isinstance(child, TableBodyElement):247            self.body = child248        else:249            raise RuntimeError("Couldn't process markdown table.")250        return False251 252    def __rich_console__(253        self, console: Console, options: ConsoleOptions254    ) -> RenderResult:255        table = Table(box=box.SIMPLE_HEAVY)256 257        if self.header is not None and self.header.row is not None:258            for column in self.header.row.cells:259                table.add_column(column.content)260 261        if self.body is not None:262            for row in self.body.rows:263                row_content = [element.content for element in row.cells]264                table.add_row(*row_content)265 266        yield table267 268 269class TableHeaderElement(MarkdownElement):270    """MarkdownElement corresponding to `thead_open` and `thead_close`."""271 272    def __init__(self) -> None:273        self.row: TableRowElement | None = None274 275    def on_child_close(276        self, context: "MarkdownContext", child: "MarkdownElement"277    ) -> bool:278        assert isinstance(child, TableRowElement)279        self.row = child280        return False281 282 283class TableBodyElement(MarkdownElement):284    """MarkdownElement corresponding to `tbody_open` and `tbody_close`."""285 286    def __init__(self) -> None:287        self.rows: list[TableRowElement] = []288 289    def on_child_close(290        self, context: "MarkdownContext", child: "MarkdownElement"291    ) -> bool:292        assert isinstance(child, TableRowElement)293        self.rows.append(child)294        return False295 296 297class TableRowElement(MarkdownElement):298    """MarkdownElement corresponding to `tr_open` and `tr_close`."""299 300    def __init__(self) -> None:301        self.cells: List[TableDataElement] = []302 303    def on_child_close(304        self, context: "MarkdownContext", child: "MarkdownElement"305    ) -> bool:306        assert isinstance(child, TableDataElement)307        self.cells.append(child)308        return False309 310 311class TableDataElement(MarkdownElement):312    """MarkdownElement corresponding to `td_open` and `td_close`313    and `th_open` and `th_close`."""314 315    @classmethod316    def create(cls, markdown: "Markdown", token: Token) -> "MarkdownElement":317        style = str(token.attrs.get("style")) or ""318 319        justify: JustifyMethod320        if "text-align:right" in style:321            justify = "right"322        elif "text-align:center" in style:323            justify = "center"324        elif "text-align:left" in style:325            justify = "left"326        else:327            justify = "default"328 329        assert justify in get_args(JustifyMethod)330        return cls(justify=justify)331 332    def __init__(self, justify: JustifyMethod) -> None:333        self.content: Text = Text("", justify=justify)334        self.justify = justify335 336    def on_text(self, context: "MarkdownContext", text: TextType) -> None:337        text = Text(text) if isinstance(text, str) else text338        text.stylize(context.current_style)339        self.content.append_text(text)340 341 342class ListElement(MarkdownElement):343    """A list element."""344 345    @classmethod346    def create(cls, markdown: "Markdown", token: Token) -> "ListElement":347        return cls(token.type, int(token.attrs.get("start", 1)))348 349    def __init__(self, list_type: str, list_start: int | None) -> None:350        self.items: List[ListItem] = []351        self.list_type = list_type352        self.list_start = list_start353 354    def on_child_close(355        self, context: "MarkdownContext", child: "MarkdownElement"356    ) -> bool:357        assert isinstance(child, ListItem)358        self.items.append(child)359        return False360 361    def __rich_console__(362        self, console: Console, options: ConsoleOptions363    ) -> RenderResult:364        if self.list_type == "bullet_list_open":365            for item in self.items:366                yield from item.render_bullet(console, options)367        else:368            number = 1 if self.list_start is None else self.list_start369            last_number = number + len(self.items)370            for index, item in enumerate(self.items):371                yield from item.render_number(372                    console, options, number + index, last_number373                )374 375 376class ListItem(TextElement):377    """An item in a list."""378 379    style_name = "markdown.item"380 381    def __init__(self) -> None:382        self.elements: Renderables = Renderables()383 384    def on_child_close(385        self, context: "MarkdownContext", child: "MarkdownElement"386    ) -> bool:387        self.elements.append(child)388        return False389 390    def render_bullet(self, console: Console, options: ConsoleOptions) -> RenderResult:391        render_options = options.update(width=options.max_width - 3)392        lines = console.render_lines(self.elements, render_options, style=self.style)393        bullet_style = console.get_style("markdown.item.bullet", default="none")394 395        bullet = Segment(" • ", bullet_style)396        padding = Segment(" " * 3, bullet_style)397        new_line = Segment("\n")398        for first, line in loop_first(lines):399            yield bullet if first else padding400            yield from line401            yield new_line402 403    def render_number(404        self, console: Console, options: ConsoleOptions, number: int, last_number: int405    ) -> RenderResult:406        number_width = len(str(last_number)) + 2407        render_options = options.update(width=options.max_width - number_width)408        lines = console.render_lines(self.elements, render_options, style=self.style)409        number_style = console.get_style("markdown.item.number", default="none")410 411        new_line = Segment("\n")412        padding = Segment(" " * number_width, number_style)413        numeral = Segment(f"{number}".rjust(number_width - 1) + " ", number_style)414        for first, line in loop_first(lines):415            yield numeral if first else padding416            yield from line417            yield new_line418 419 420class Link(TextElement):421    @classmethod422    def create(cls, markdown: "Markdown", token: Token) -> "MarkdownElement":423        url = token.attrs.get("href", "#")424        return cls(token.content, str(url))425 426    def __init__(self, text: str, href: str):427        self.text = Text(text)428        self.href = href429 430 431class ImageItem(TextElement):432    """Renders a placeholder for an image."""433 434    new_line = False435 436    @classmethod437    def create(cls, markdown: "Markdown", token: Token) -> "MarkdownElement":438        """Factory to create markdown element,439 440        Args:441            markdown (Markdown): The parent Markdown object.442            token (Any): A token from markdown-it.443 444        Returns:445            MarkdownElement: A new markdown element446        """447        return cls(str(token.attrs.get("src", "")), markdown.hyperlinks)448 449    def __init__(self, destination: str, hyperlinks: bool) -> None:450        self.destination = destination451        self.hyperlinks = hyperlinks452        self.link: Optional[str] = None453        super().__init__()454 455    def on_enter(self, context: "MarkdownContext") -> None:456        self.link = context.current_style.link457        self.text = Text(justify="left")458        super().on_enter(context)459 460    def __rich_console__(461        self, console: Console, options: ConsoleOptions462    ) -> RenderResult:463        link_style = Style(link=self.link or self.destination or None)464        title = self.text or Text(self.destination.strip("/").rsplit("/", 1)[-1])465        if self.hyperlinks:466            title.stylize(link_style)467        text = Text.assemble("🌆 ", title, " ", end="")468        yield text469 470 471class MarkdownContext:472    """Manages the console render state."""473 474    def __init__(475        self,476        console: Console,477        options: ConsoleOptions,478        style: Style,479        inline_code_lexer: Optional[str] = None,480        inline_code_theme: str = "monokai",481    ) -> None:482        self.console = console483        self.options = options484        self.style_stack: StyleStack = StyleStack(style)485        self.stack: Stack[MarkdownElement] = Stack()486 487        self._syntax: Optional[Syntax] = None488        if inline_code_lexer is not None:489            self._syntax = Syntax("", inline_code_lexer, theme=inline_code_theme)490 491    @property492    def current_style(self) -> Style:493        """Current style which is the product of all styles on the stack."""494        return self.style_stack.current495 496    def on_text(self, text: str, node_type: str) -> None:497        """Called when the parser visits text."""498        if node_type in {"fence", "code_inline"} and self._syntax is not None:499            highlight_text = self._syntax.highlight(text)500            highlight_text.rstrip()501            self.stack.top.on_text(502                self, Text.assemble(highlight_text, style=self.style_stack.current)503            )504        else:505            self.stack.top.on_text(self, text)506 507    def enter_style(self, style_name: Union[str, Style]) -> Style:508        """Enter a style context."""509        style = self.console.get_style(style_name, default="none")510        self.style_stack.push(style)511        return self.current_style512 513    def leave_style(self) -> Style:514        """Leave a style context."""515        style = self.style_stack.pop()516        return style517 518 519class Markdown(JupyterMixin):520    """A Markdown renderable.521 522    Args:523        markup (str): A string containing markdown.524        code_theme (str, optional): Pygments theme for code blocks. Defaults to "monokai".525        justify (JustifyMethod, optional): Justify value for paragraphs. Defaults to None.526        style (Union[str, Style], optional): Optional style to apply to markdown.527        hyperlinks (bool, optional): Enable hyperlinks. Defaults to ``True``.528        inline_code_lexer: (str, optional): Lexer to use if inline code highlighting is529            enabled. Defaults to None.530        inline_code_theme: (Optional[str], optional): Pygments theme for inline code531            highlighting, or None for no highlighting. Defaults to None.532    """533 534    elements: ClassVar[Dict[str, Type[MarkdownElement]]] = {535        "paragraph_open": Paragraph,536        "heading_open": Heading,537        "fence": CodeBlock,538        "code_block": CodeBlock,539        "blockquote_open": BlockQuote,540        "hr": HorizontalRule,541        "bullet_list_open": ListElement,542        "ordered_list_open": ListElement,543        "list_item_open": ListItem,544        "image": ImageItem,545        "table_open": TableElement,546        "tbody_open": TableBodyElement,547        "thead_open": TableHeaderElement,548        "tr_open": TableRowElement,549        "td_open": TableDataElement,550        "th_open": TableDataElement,551    }552 553    inlines = {"em", "strong", "code", "s"}554 555    def __init__(556        self,557        markup: str,558        code_theme: str = "monokai",559        justify: Optional[JustifyMethod] = None,560        style: Union[str, Style] = "none",561        hyperlinks: bool = True,562        inline_code_lexer: Optional[str] = None,563        inline_code_theme: Optional[str] = None,564    ) -> None:565        parser = MarkdownIt().enable("strikethrough").enable("table")566        self.markup = markup567        self.parsed = parser.parse(markup)568        self.code_theme = code_theme569        self.justify: Optional[JustifyMethod] = justify570        self.style = style571        self.hyperlinks = hyperlinks572        self.inline_code_lexer = inline_code_lexer573        self.inline_code_theme = inline_code_theme or code_theme574 575    def _flatten_tokens(self, tokens: Iterable[Token]) -> Iterable[Token]:576        """Flattens the token stream."""577        for token in tokens:578            is_fence = token.type == "fence"579            is_image = token.tag == "img"580            if token.children and not (is_image or is_fence):581                yield from self._flatten_tokens(token.children)582            else:583                yield token584 585    def __rich_console__(586        self, console: Console, options: ConsoleOptions587    ) -> RenderResult:588        """Render markdown to the console."""589        style = console.get_style(self.style, default="none")590        options = options.update(height=None)591        context = MarkdownContext(592            console,593            options,594            style,595            inline_code_lexer=self.inline_code_lexer,596            inline_code_theme=self.inline_code_theme,597        )598        tokens = self.parsed599        inline_style_tags = self.inlines600        new_line = False601        _new_line_segment = Segment.line()602 603        for token in self._flatten_tokens(tokens):604            node_type = token.type605            tag = token.tag606 607            entering = token.nesting == 1608            exiting = token.nesting == -1609            self_closing = token.nesting == 0610 611            if node_type == "text":612                context.on_text(token.content, node_type)613            elif node_type == "hardbreak":614                context.on_text("\n", node_type)615            elif node_type == "softbreak":616                context.on_text(" ", node_type)617            elif node_type == "link_open":618                href = str(token.attrs.get("href", ""))619                if self.hyperlinks:620                    link_style = console.get_style("markdown.link_url", default="none")621                    link_style += Style(link=href)622                    context.enter_style(link_style)623                else:624                    context.stack.push(Link.create(self, token))625            elif node_type == "link_close":626                if self.hyperlinks:627                    context.leave_style()628                else:629                    element = context.stack.pop()630                    assert isinstance(element, Link)631                    link_style = console.get_style("markdown.link", default="none")632                    context.enter_style(link_style)633                    context.on_text(element.text.plain, node_type)634                    context.leave_style()635                    context.on_text(" (", node_type)636                    link_url_style = console.get_style(637                        "markdown.link_url", default="none"638                    )639                    context.enter_style(link_url_style)640                    context.on_text(element.href, node_type)641                    context.leave_style()642                    context.on_text(")", node_type)643            elif (644                tag in inline_style_tags645                and node_type != "fence"646                and node_type != "code_block"647            ):648                if entering:649                    # If it's an opening inline token e.g. strong, em, etc.650                    # Then we move into a style context i.e. push to stack.651                    context.enter_style(f"markdown.{tag}")652                elif exiting:653                    # If it's a closing inline style, then we pop the style654                    # off of the stack, to move out of the context of it...655                    context.leave_style()656                else:657                    # If it's a self-closing inline style e.g. `code_inline`658                    context.enter_style(f"markdown.{tag}")659                    if token.content:660                        context.on_text(token.content, node_type)661                    context.leave_style()662            else:663                # Map the markdown tag -> MarkdownElement renderable664                element_class = self.elements.get(token.type) or UnknownElement665                element = element_class.create(self, token)666 667                if entering or self_closing:668                    context.stack.push(element)669                    element.on_enter(context)670 671                if exiting:  # CLOSING tag672                    element = context.stack.pop()673 674                    should_render = not context.stack or (675                        context.stack676                        and context.stack.top.on_child_close(context, element)677                    )678 679                    if should_render:680                        if new_line:681                            yield _new_line_segment682 683                        yield from console.render(element, context.options)684                elif self_closing:  # SELF-CLOSING tags (e.g. text, code, image)685                    context.stack.pop()686                    text = token.content687                    if text is not None:688                        element.on_text(context, text)689 690                    should_render = (691                        not context.stack692                        or context.stack693                        and context.stack.top.on_child_close(context, element)694                    )695                    if should_render:696                        if new_line:697                            yield _new_line_segment698                        yield from console.render(element, context.options)699 700                if exiting or self_closing:701                    element.on_leave(context)702                    new_line = element.new_line703 704 705if __name__ == "__main__":  # pragma: no cover706    import argparse707    import sys708 709    parser = argparse.ArgumentParser(710        description="Render Markdown to the console with Rich"711    )712    parser.add_argument(713        "path",714        metavar="PATH",715        help="path to markdown file, or - for stdin",716    )717    parser.add_argument(718        "-c",719        "--force-color",720        dest="force_color",721        action="store_true",722        default=None,723        help="force color for non-terminals",724    )725    parser.add_argument(726        "-t",727        "--code-theme",728        dest="code_theme",729        default="monokai",730        help="pygments code theme",731    )732    parser.add_argument(733        "-i",734        "--inline-code-lexer",735        dest="inline_code_lexer",736        default=None,737        help="inline_code_lexer",738    )739    parser.add_argument(740        "-y",741        "--hyperlinks",742        dest="hyperlinks",743        action="store_true",744        help="enable hyperlinks",745    )746    parser.add_argument(747        "-w",748        "--width",749        type=int,750        dest="width",751        default=None,752        help="width of output (default will auto-detect)",753    )754    parser.add_argument(755        "-j",756        "--justify",757        dest="justify",758        action="store_true",759        help="enable full text justify",760    )761    parser.add_argument(762        "-p",763        "--page",764        dest="page",765        action="store_true",766        help="use pager to scroll output",767    )768    args = parser.parse_args()769 770    from rich.console import Console771 772    if args.path == "-":773        markdown_body = sys.stdin.read()774    else:775        with open(args.path, "rt", encoding="utf-8") as markdown_file:776            markdown_body = markdown_file.read()777 778    markdown = Markdown(779        markdown_body,780        justify="full" if args.justify else "left",781        code_theme=args.code_theme,782        hyperlinks=args.hyperlinks,783        inline_code_lexer=args.inline_code_lexer,784    )785    if args.page:786        import io787        import pydoc788 789        fileio = io.StringIO()790        console = Console(791            file=fileio, force_terminal=args.force_color, width=args.width792        )793        console.print(markdown)794        pydoc.pager(fileio.getvalue())795 796    else:797        console = Console(798            force_terminal=args.force_color, width=args.width, record=True799        )800        console.print(markdown)801 
codekingpro/portable-devtools · Team Ai