Skip to content

xnano.utils.markup

xnano.utils.markup

xnano.utils.markup


Parse ANSI, highlighted source code, and Markdown into styled text runs.

Functions:

MarkdownBlock module-attribute

MarkdownBlock: TypeAlias = (
    "tuple[str, tuple[tuple[Run, ...], ...]] | tuple[str, str, str]"
)

One parsed block: ("text", lines) or ("image", src, alt).

strip_ansi_escapes

strip_ansi_escapes(content: str) -> str

Return content with all ANSI escape sequences removed.

Source code in xnano/utils/markup.py
def strip_ansi_escapes(content: str) -> str:
    """Return ``content`` with all ANSI escape sequences removed."""
    return _ANSI_ESCAPE_PATTERN.sub("", content)

parse_ansi_lines cached

parse_ansi_lines(
    content: str,
) -> tuple[tuple[Run, ...], ...]

Parse ANSI-escaped content into styled Run lines.

SGR (color/style) sequences become run styles carried across lines; all other escape sequences are stripped, not emulated — this is a log view, not a terminal emulator.

Parameters:

  • content (str) –

    Raw text containing ANSI escape sequences.

Returns:

  • tuple[tuple[Run, ...], ...]

    One tuple of Run spans per source line.

Source code in xnano/utils/markup.py
@functools.lru_cache(maxsize=128)
def parse_ansi_lines(content: str) -> tuple[tuple[Run, ...], ...]:
    """Parse ANSI-escaped content into styled ``Run`` lines.

    SGR (color/style) sequences become run styles carried across
    lines; all other escape sequences are stripped, not emulated —
    this is a log view, not a terminal emulator.

    Args:
        content: Raw text containing ANSI escape sequences.

    Returns:
        One tuple of ``Run`` spans per source line.
    """
    state: _SgrState = (None, None, ())
    lines: list[tuple[Run, ...]] = []
    runs: list[Run] = []

    def emit(text: str) -> None:
        color, background, modifiers = state
        for index, segment in enumerate(text.split("\n")):
            if index > 0:
                lines.append(tuple(runs))
                runs.clear()
            if segment:
                runs.append(
                    Run(
                        text=segment,
                        foreground=color,
                        background=background,
                        modifiers=modifiers,
                    )
                )

    position = 0
    for match in _SGR_PATTERN.finditer(content):
        emit(strip_ansi_escapes(content[position : match.start()]))
        position = match.end()
        codes = [
            int(part) if part else 0
            for part in (match.group(1) or "0").split(";")
        ]
        state = _apply_sgr_codes(codes, state)
    emit(strip_ansi_escapes(content[position:]))
    lines.append(tuple(runs))
    return tuple(lines)

highlight_lines cached

highlight_lines(
    content: str, language: str
) -> tuple[tuple[Run, ...], ...]

Syntax-highlight content into styled Run lines.

Parameters:

  • content (str) –

    Source code to highlight.

  • language (str) –

    A Pygments lexer name ("python", "rust", …). Unknown names fall back to plain text.

Returns:

  • tuple[tuple[Run, ...], ...]

    One tuple of Run spans per source line.

Source code in xnano/utils/markup.py
@functools.lru_cache(maxsize=64)
def highlight_lines(
    content: str,
    language: str,
) -> tuple[tuple[Run, ...], ...]:
    """Syntax-highlight ``content`` into styled ``Run`` lines.

    Args:
        content: Source code to highlight.
        language: A Pygments lexer name (``"python"``, ``"rust"``, …).
            Unknown names fall back to plain text.

    Returns:
        One tuple of ``Run`` spans per source line.
    """
    import pygments.lexers
    import pygments.util

    try:
        lexer = pygments.lexers.get_lexer_by_name(language)
    except pygments.util.ClassNotFound:
        return tuple(
            (Run(text=line),) if line else () for line in content.split("\n")
        )

    lines: list[tuple[Run, ...]] = []
    runs: list[Run] = []
    for token_type, token_text in lexer.get_tokens(content):
        color, modifiers = _style_for_token(token_type)
        for index, segment in enumerate(token_text.split("\n")):
            if index > 0:
                lines.append(tuple(runs))
                runs.clear()
            if segment:
                runs.append(
                    Run(text=segment, foreground=color, modifiers=modifiers)
                )
    if runs:
        lines.append(tuple(runs))
    # Pygments always terminates output with a newline; drop the
    # resulting phantom empty last line unless the source had one.
    if lines and not content.endswith("\n") and lines[-1] == ():
        lines.pop()
    return tuple(lines)

markdown_lines cached

markdown_lines(content: str) -> tuple[tuple[Run, ...], ...]

Parse markdown content into styled Run lines.

Headings render bold in an accent color, emphasis/strong map to italic/bold, list items get bullets, blockquotes render dim, inline code renders reversed, and fenced code blocks are syntax-highlighted by their fence language tag. Tables and images are out of scope and render as their plain text.

Parameters:

  • content (str) –

    Markdown source.

Returns:

  • tuple[tuple[Run, ...], ...]

    One tuple of Run spans per rendered line.

Source code in xnano/utils/markup.py
@functools.lru_cache(maxsize=64)
def markdown_lines(content: str) -> tuple[tuple[Run, ...], ...]:
    """Parse markdown ``content`` into styled ``Run`` lines.

    Headings render bold in an accent color, emphasis/strong map to
    italic/bold, list items get bullets, blockquotes render dim,
    inline code renders reversed, and fenced code blocks are
    syntax-highlighted by their fence language tag. Tables and images
    are out of scope and render as their plain text.

    Args:
        content: Markdown source.

    Returns:
        One tuple of ``Run`` spans per rendered line.
    """
    import markdown_it

    lines: list[tuple[Run, ...]] = []
    quote_depth = 0
    list_depth = 0
    heading_level = 0

    for token in markdown_it.MarkdownIt("commonmark").parse(content):
        if token.type == "blockquote_open":
            quote_depth += 1
        elif token.type == "blockquote_close":
            quote_depth -= 1
        elif token.type in ("bullet_list_open", "ordered_list_open"):
            list_depth += 1
        elif token.type in ("bullet_list_close", "ordered_list_close"):
            list_depth -= 1
            if list_depth == 0:
                lines.append(())
        elif token.type == "heading_open":
            heading_level = int(token.tag[1:])
        elif token.type == "heading_close":
            heading_level = 0
            lines.append(())
        elif token.type == "paragraph_close":
            if quote_depth == 0 and list_depth == 0:
                lines.append(())
        elif token.type == "inline":
            lines.append(
                _markdown_inline_line(
                    token,
                    heading_level=heading_level,
                    quote_depth=quote_depth,
                    list_depth=list_depth,
                )
            )
        elif token.type in ("fence", "code_block"):
            lines.extend(_markdown_fence_lines(token))
            lines.append(())
        elif token.type == "hr":
            lines.append((Run(text="─" * 24, modifiers=("dim",)),))
            lines.append(())

    while lines and lines[-1] == ():
        lines.pop()
    return tuple(lines)

markdown_blocks cached

markdown_blocks(content: str) -> tuple[Any, ...]

Parse markdown into an ordered list of text and image blocks.

Text is grouped into ("text", lines) blocks of styled runs, the same as :func:markdown_lines. Each Markdown image is split out into its own ("image", src, alt) block so a document viewer can render it as a real terminal image between the surrounding text.

Parameters:

  • content (str) –

    Markdown source.

Returns:

  • Any

    A tuple of ("text", lines) and ("image", src, alt) blocks

  • ...

    in document order.

Source code in xnano/utils/markup.py
@functools.lru_cache(maxsize=64)
def markdown_blocks(content: str) -> tuple[Any, ...]:
    """Parse markdown into an ordered list of text and image blocks.

    Text is grouped into ``("text", lines)`` blocks of styled runs, the
    same as :func:`markdown_lines`. Each Markdown image is split out into
    its own ``("image", src, alt)`` block so a document viewer can render
    it as a real terminal image between the surrounding text.

    Args:
        content: Markdown source.

    Returns:
        A tuple of ``("text", lines)`` and ``("image", src, alt)`` blocks
        in document order.
    """
    import markdown_it

    blocks: list[Any] = []
    text_lines: list[tuple[Run, ...]] = []
    quote_depth = 0
    list_depth = 0
    heading_level = 0

    def flush_text() -> None:
        while text_lines and text_lines[-1] == ():
            text_lines.pop()
        if text_lines:
            blocks.append(("text", tuple(text_lines)))
        text_lines.clear()

    for token in markdown_it.MarkdownIt("commonmark").parse(content):
        if token.type == "blockquote_open":
            quote_depth += 1
        elif token.type == "blockquote_close":
            quote_depth -= 1
        elif token.type in ("bullet_list_open", "ordered_list_open"):
            list_depth += 1
        elif token.type in ("bullet_list_close", "ordered_list_close"):
            list_depth -= 1
            if list_depth == 0:
                text_lines.append(())
        elif token.type == "heading_open":
            heading_level = int(token.tag[1:])
        elif token.type == "heading_close":
            heading_level = 0
            text_lines.append(())
        elif token.type == "paragraph_close":
            if quote_depth == 0 and list_depth == 0:
                text_lines.append(())
        elif token.type == "inline":
            images = [
                child
                for child in (token.children or ())
                if child.type == "image"
            ]
            if images:
                line = _markdown_inline_line(
                    token,
                    heading_level=heading_level,
                    quote_depth=quote_depth,
                    list_depth=list_depth,
                )
                if "".join(run.text for run in line).strip():
                    text_lines.append(line)
                for image in images:
                    flush_text()
                    source = image.attrGet("src") or ""
                    blocks.append(
                        ("image", str(source), (image.content or "").strip())
                    )
            else:
                text_lines.append(
                    _markdown_inline_line(
                        token,
                        heading_level=heading_level,
                        quote_depth=quote_depth,
                        list_depth=list_depth,
                    )
                )
        elif token.type in ("fence", "code_block"):
            text_lines.extend(_markdown_fence_lines(token))
            text_lines.append(())
        elif token.type == "hr":
            text_lines.append((Run(text="─" * 24, modifiers=("dim",)),))
            text_lines.append(())

    flush_text()
    return tuple(blocks)