Skip to content

xnano.markdown

xnano.markdown

xnano.markdown


Load Markdown from text or a file, render one frame, or page through it interactively.

The interactive viewer is a pager, not a one-shot render: it windows the rendered lines to the viewport and moves that window in response to the keyboard (arrows, j/k, PageUp/PageDown, space, Home/End, g/G) and the mouse wheel, with q to quit. A document taller than the screen can therefore be scrolled, which a plain render cannot do.

Inline images paint as compact, never-upscaled half-block thumbnails. Hovering (or pressing i / clicking) expands the active image toward native cell resolution for a clearer look without paying full-size decode cost for every picture on every frame.

Classes:

Functions:

MarkdownViewport dataclass

MarkdownViewport(
    content: str | Text | list[str | Text] = "",
    foreground: ColorLike | None = None,
    background: ColorLike | None = None,
    modifiers: tuple[CharacterModifier, ...] = (),
    horizontal_align: Alignment | None = None,
    vertical_align: VerticalAlignment | None = None,
    wrap: bool = True,
    input: bool = False,
    placeholder: str | Text | None = None,
    cursor: int | None = None,
    multiline: bool = False,
    rows: int | None = None,
    ansi: bool = False,
    markdown: bool = False,
    language: str | None = None,
    passthrough: Sequence[str] = (),
    mask: str | None = None,
    max_length: int | None = None,
    read_only: bool = False,
    tab_size: int = 4,
    focusable: bool = False,
    fill: bool = False,
    base_path: Path | None = None,
    images: bool = True,
    links: bool = True,
    offset: int = 0,
    reserved_rows: int = _STATUS_ROWS,
    image_rows: int = _THUMB_MAX_ROWS,
    thumb_max_cols: int = _THUMB_MAX_COLS,
    thumb_max_rows: int = _THUMB_MAX_ROWS,
    expand_max_cols: int = _EXPAND_MAX_COLS,
    expand_max_rows: int = _EXPAND_MAX_ROWS,
    *,
    visible: bool = True,
    z: int = 0,
    fit_content: bool = True
)

Bases: Markdown

Block-aware Markdown windowed to a scroll offset.

The document is parsed into an ordered run of text and image blocks (see markdown_blocks); each frame composes only the blocks that fall inside [offset : offset + viewport]. Text blocks paint their visible line slice and image blocks paint through the real :class:~xnano.components.image.Image component, so a picture or GIF renders inline rather than being dropped.

Images default to compact, never-upscaled thumbnails with a one-line caption. Hovering the pointer over a thumbnail (or pinning with a click / i) expands that one image toward native cell resolution for a clearer view — only the active image pays the larger compose cost.

Attributes:

Methods:

  • viewport_height

    Return the visible row count for the current terminal size.

  • total_rows

    Return the document height in rows across all blocks.

  • max_offset

    Return the largest useful offset — the last full page of rows.

  • page_rows

    Return the offset delta for a page-sized jump (one row overlap).

  • scroll_percentage

    Return how far the view has scrolled, from 0 to 100.

  • scroll_by

    Move the view by delta rows, clamped to the document.

  • scroll_to_top

    Move the view to the first row.

  • scroll_to_bottom

    Move the view to the last page.

  • has_expandable_image

    Return whether the document contains at least one local image.

  • active_image_key

    Return the pinned or hovered image key, if any.

  • update_pointer

    Update hover from body-local cell coordinates.

  • clear_pointer

    Clear hover state.

  • toggle_pin_at

    Pin or unpin the image under body-local coordinates.

  • toggle_expand

    Toggle expand on the hovered, pinned, or first visible image.

  • compose

    Compose the blocks currently inside the viewport.

  • component_post_init

    Force markdown mode, then run Text setup.

  • get_frame

    Optional frame/panel chrome around composed content.

  • get_size

    Return the preferred cell size of this component.

  • before_render

    Called before rendering; returns the effective render area.

  • after_render

    Record the multi-line editor caret so the terminal cursor tracks it.

  • compose_extra_small

    Compose content when the viewport is extra small (< 40 cols).

  • compose_small

    Compose content when the viewport is small (40–79 cols).

  • compose_medium

    Compose content when the viewport is medium (80–119 cols).

  • compose_large

    Compose content when the viewport is large (120–159 cols).

  • compose_extra_large

    Compose content when the viewport is extra large (>= 160 cols).

  • handle_keyboard

    Edit this text when it has focus.

  • handle_paste

    Insert pasted text at the caret of a multi-line editor.

  • get_terminal_node

    Return composed content for terminal compatibility.

offset class-attribute instance-attribute

offset: int = 0

First document row shown at the top of the view.

reserved_rows class-attribute instance-attribute

reserved_rows: int = _STATUS_ROWS

Rows the owning viewer reserves below the body (its status line).

image_rows class-attribute instance-attribute

image_rows: int = _THUMB_MAX_ROWS

Fallback thumbnail rows when image dimensions cannot be read.

thumb_max_cols class-attribute instance-attribute

thumb_max_cols: int = _THUMB_MAX_COLS

Maximum columns for collapsed image thumbnails.

thumb_max_rows class-attribute instance-attribute

thumb_max_rows: int = _THUMB_MAX_ROWS

Maximum body rows for collapsed image thumbnails.

expand_max_cols class-attribute instance-attribute

expand_max_cols: int = _EXPAND_MAX_COLS

Maximum columns for the hover/pin expanded preview.

expand_max_rows class-attribute instance-attribute

expand_max_rows: int = _EXPAND_MAX_ROWS

Maximum body rows for the hover/pin expanded preview.

visible class-attribute instance-attribute

visible: bool = dataclasses.field(
    default=True, kw_only=True
)

Whether this component paints at all.

z class-attribute instance-attribute

z: int = dataclasses.field(default=0, kw_only=True)

Stacking order among sibling content.

fit_content class-attribute instance-attribute

fit_content: bool = dataclasses.field(
    default=True, kw_only=True
)

When True, paint at natural size inside a larger slot.

focused property

focused: bool

Whether this component currently holds field focus.

content class-attribute instance-attribute

content: str | Text | list[str | Text] = dataclasses.field(
    default=""
)

Plain string, nested Text, or a list of either.

foreground class-attribute instance-attribute

foreground: ColorLike | None = None

Foreground color (deprecated alias: color).

background class-attribute instance-attribute

background: ColorLike | None = None

Background color.

modifiers class-attribute instance-attribute

modifiers: tuple[CharacterModifier, ...] = ()

Character modifiers such as bold or underline.

horizontal_align class-attribute instance-attribute

horizontal_align: Alignment | None = None

Horizontal alignment at the paragraph level.

vertical_align class-attribute instance-attribute

vertical_align: VerticalAlignment | None = None

Vertical alignment within the painted area.

wrap class-attribute instance-attribute

wrap: bool = True

Whether long lines may wrap.

input class-attribute instance-attribute

input: bool = False

When True on a leaf, the component is an editable field.

placeholder class-attribute instance-attribute

placeholder: str | Text | None = None

Shown when input is empty and unfocused.

cursor class-attribute instance-attribute

cursor: int | None = None

Caret index for single-line input; None means end of string.

multiline class-attribute instance-attribute

multiline: bool = False

When True with input, editing uses CoreTextEditor.

rows class-attribute instance-attribute

rows: int | None = None

Preferred visible height (lines) for a multiline input.

ansi class-attribute instance-attribute

ansi: bool = False

Parse ANSI SGR sequences in leaf content.

markdown class-attribute instance-attribute

markdown: bool = False

Parse leaf content as markdown.

language class-attribute instance-attribute

language: str | None = None

Pygments lexer name for syntax highlighting only.

passthrough class-attribute instance-attribute

passthrough: Sequence[str] = ()

Key bindings this input never captures while focused.

mask class-attribute instance-attribute

mask: str | None = None

Display-only mask character(s); real value is preserved.

max_length class-attribute instance-attribute

max_length: int | None = None

Maximum plain-string length; longer input is clamped.

read_only class-attribute instance-attribute

read_only: bool = False

Reject edits while remaining focusable.

tab_size class-attribute instance-attribute

tab_size: int = 4

Tab width applied to multiline tab insertion and paste.

focusable class-attribute instance-attribute

focusable: bool = False

Whether this component participates in field focus.

fill class-attribute instance-attribute

fill: bool = False

Whether background fills the whole slot rather than only the text glyphs. When True short lines still paint the background across the full cell width (no manual right-padding required).

owns_cursor property

owns_cursor: bool

Whether this Text paints its own caret (multi-line editor).

cursor_position property

cursor_position: tuple[int, int] | None

Absolute caret cell for the terminal cursor, set during paint.

Reported only for a focused multi-line editor (a single-line input paints its own caret inline). None otherwise, which hides the hardware cursor.

value property writable

value: str

Canonical plain-string content for leaf and input modes.

base_path class-attribute instance-attribute

base_path: Path | None = None

Root path for resolving relative image and link targets.

images class-attribute instance-attribute

images: bool = True

Whether image constructs may be emitted when supported.

links: bool = True

Whether link constructs may be emitted when supported.

viewport_height

viewport_height() -> int

Return the visible row count for the current terminal size.

Source code in xnano/markdown.py
def viewport_height(self) -> int:
    """Return the visible row count for the current terminal size."""
    from xnano.core.runtime import get_active_runtime

    runtime = get_active_runtime()
    height = runtime.size[1] if runtime is not None else 24
    return max(1, height - self.reserved_rows)

total_rows

total_rows() -> int

Return the document height in rows across all blocks.

Source code in xnano/markdown.py
def total_rows(self) -> int:
    """Return the document height in rows across all blocks."""
    width = self._layout_width
    viewport = self.viewport_height()
    self._sync_image_block_rows(
        terminal_width=width,
        viewport_rows=viewport,
    )
    return sum(block.rows for block in self._blocks())

max_offset

max_offset() -> int

Return the largest useful offset — the last full page of rows.

Source code in xnano/markdown.py
def max_offset(self) -> int:
    """Return the largest useful offset — the last full page of rows."""
    return max(0, self.total_rows() - self.viewport_height())

page_rows

page_rows() -> int

Return the offset delta for a page-sized jump (one row overlap).

Source code in xnano/markdown.py
def page_rows(self) -> int:
    """Return the offset delta for a page-sized jump (one row overlap)."""
    return max(1, self.viewport_height() - 1)

scroll_percentage

scroll_percentage() -> int

Return how far the view has scrolled, from 0 to 100.

Source code in xnano/markdown.py
def scroll_percentage(self) -> int:
    """Return how far the view has scrolled, from 0 to 100."""
    maximum = self.max_offset()
    if maximum <= 0:
        return 100
    return round(min(self.offset, maximum) / maximum * 100)

scroll_by

scroll_by(delta: int) -> None

Move the view by delta rows, clamped to the document.

Source code in xnano/markdown.py
def scroll_by(self, delta: int) -> None:
    """Move the view by ``delta`` rows, clamped to the document."""
    self.offset = min(self.max_offset(), max(0, self.offset + delta))

scroll_to_top

scroll_to_top() -> None

Move the view to the first row.

Source code in xnano/markdown.py
def scroll_to_top(self) -> None:
    """Move the view to the first row."""
    self.offset = 0

scroll_to_bottom

scroll_to_bottom() -> None

Move the view to the last page.

Source code in xnano/markdown.py
def scroll_to_bottom(self) -> None:
    """Move the view to the last page."""
    self.offset = self.max_offset()

has_expandable_image

has_expandable_image() -> bool

Return whether the document contains at least one local image.

Source code in xnano/markdown.py
def has_expandable_image(self) -> bool:
    """Return whether the document contains at least one local image."""
    return any(block.kind == "image" for block in self._blocks())

active_image_key

active_image_key() -> str | None

Return the pinned or hovered image key, if any.

Source code in xnano/markdown.py
def active_image_key(self) -> str | None:
    """Return the pinned or hovered image key, if any."""
    return self._pinned_image_key or self._hovered_image_key

update_pointer

update_pointer(x: int, y: int) -> bool

Update hover from body-local cell coordinates.

Parameters:

  • x (int) –

    Column within the body field.

  • y (int) –

    Row within the body field (0 = top of the viewport).

Returns:

  • bool

    True when the hovered image identity changed.

Source code in xnano/markdown.py
def update_pointer(self, x: int, y: int) -> bool:
    """Update hover from body-local cell coordinates.

    Args:
        x: Column within the body field.
        y: Row within the body field (0 = top of the viewport).

    Returns:
        ``True`` when the hovered image identity changed.
    """
    document_row = min(self.offset, self.max_offset()) + max(0, y)
    previous = self._hovered_image_key
    hit_key: str | None = None
    for region in self._image_hit_regions:
        if (
            region.start_row <= document_row < region.end_row
            and 0 <= x < max(1, region.width + 2)
        ):
            hit_key = region.key
            break
    self._hovered_image_key = hit_key
    return previous != hit_key

clear_pointer

clear_pointer() -> bool

Clear hover state.

Returns:

  • bool

    True when a hovered image was cleared.

Source code in xnano/markdown.py
def clear_pointer(self) -> bool:
    """Clear hover state.

    Returns:
        ``True`` when a hovered image was cleared.
    """
    if self._hovered_image_key is None:
        return False
    self._hovered_image_key = None
    return True

toggle_pin_at

toggle_pin_at(x: int, y: int) -> bool

Pin or unpin the image under body-local coordinates.

Parameters:

  • x (int) –

    Column within the body field.

  • y (int) –

    Row within the body field.

Returns:

  • bool

    True when pin state changed.

Source code in xnano/markdown.py
def toggle_pin_at(self, x: int, y: int) -> bool:
    """Pin or unpin the image under body-local coordinates.

    Args:
        x: Column within the body field.
        y: Row within the body field.

    Returns:
        ``True`` when pin state changed.
    """
    document_row = min(self.offset, self.max_offset()) + max(0, y)
    for region in self._image_hit_regions:
        if (
            region.start_row <= document_row < region.end_row
            and 0 <= x < max(1, region.width + 2)
        ):
            if self._pinned_image_key == region.key:
                self._pinned_image_key = None
            else:
                self._pinned_image_key = region.key
                self._hovered_image_key = region.key
            return True
    return False

toggle_expand

toggle_expand() -> bool

Toggle expand on the hovered, pinned, or first visible image.

Returns:

  • bool

    True when pin state changed.

Source code in xnano/markdown.py
def toggle_expand(self) -> bool:
    """Toggle expand on the hovered, pinned, or first visible image.

    Returns:
        ``True`` when pin state changed.
    """
    if self._pinned_image_key is not None:
        self._pinned_image_key = None
        return True
    if self._hovered_image_key is not None:
        self._pinned_image_key = self._hovered_image_key
        return True
    # Fall back to the first image currently intersecting the viewport.
    top = min(self.offset, self.max_offset())
    bottom = top + self.viewport_height()
    for region in self._image_hit_regions:
        if region.end_row > top and region.start_row < bottom:
            self._pinned_image_key = region.key
            return True
    for block in self._blocks():
        if block.kind == "image" and block.image_key:
            self._pinned_image_key = block.image_key
            return True
    return False

compose

compose(ctx: 'ComponentRenderContext[Any]') -> Any

Compose the blocks currently inside the viewport.

Source code in xnano/markdown.py
def compose(self, ctx: "ComponentRenderContext[Any]") -> Any:
    """Compose the blocks currently inside the viewport."""
    import xnano_core.core as core
    import xnano_core.rust.native as native

    from xnano.core.content import Native
    from xnano.core.rendering import lower_content
    from xnano.core.runtime import get_active_runtime

    del ctx  # area comes from the active runtime / field slot
    viewport = self.viewport_height()
    runtime = get_active_runtime()
    width = runtime.size[0] if runtime is not None else 80
    self._layout_width = width
    self._sync_image_block_rows(
        terminal_width=width,
        viewport_rows=viewport,
    )
    top = min(self.offset, self.max_offset())
    bottom = top + viewport

    children: list[Any] = []
    constraints: list[Any] = []
    hit_regions: list[_ImageHitRegion] = []
    row = 0
    for block in self._blocks():
        start, end = row, row + block.rows
        row = end
        if block.kind == "image" and block.image_key:
            body_cols, _ = self._image_body_size(
                block,
                terminal_width=width,
                viewport_rows=viewport,
            )
            hit_regions.append(
                _ImageHitRegion(
                    key=block.image_key,
                    start_row=start,
                    end_row=end,
                    width=body_cols,
                )
            )
        if end <= top or start >= bottom:
            continue
        visible_rows = min(end, bottom) - max(start, top)
        if visible_rows <= 0:
            continue
        if block.kind == "text":
            first = max(start, top) - start
            window = block.lines[first : first + visible_rows]
            node = lower_content(
                TextBlock(
                    lines=tuple(window),
                    foreground=self.foreground,
                    background=self.background,
                    modifiers=self.modifiers,
                    horizontal_align=self.horizontal_align,
                    wrap=self.wrap,
                )
            )
        else:
            first = max(start, top) - start
            node = self._compose_image_block(
                block,
                terminal_width=width,
                viewport_rows=viewport,
                first_row=first,
                visible_rows=visible_rows,
            )
        children.append(node)
        constraints.append(native.Constraint.length(visible_rows))

    self._image_hit_regions = tuple(hit_regions)

    if not children:
        return TextBlock(lines=(), visible=self.visible)
    column = core.CoreRenderNode.column(
        children, constraints=constraints, gap=0
    )
    return Native(interface_kind="terminal", payload=column)

component_post_init

component_post_init() -> None

Force markdown mode, then run Text setup.

Source code in xnano/components/markdown.py
def component_post_init(self) -> None:
    """Force markdown mode, then run ``Text`` setup."""
    self.markdown = True
    super().component_post_init()

get_frame

get_frame() -> Any | None

Optional frame/panel chrome around composed content.

Source code in xnano/components/component.py
def get_frame(self) -> Any | None:
    """Optional frame/panel chrome around composed content."""
    return None

get_size

get_size(ctx: ComponentRenderContext[StateT]) -> Size

Return the preferred cell size of this component.

Source code in xnano/components/component.py
def get_size(self, ctx: ComponentRenderContext[StateT]) -> Size:
    """Return the preferred cell size of this component."""
    return Size(width=0, height=0)

before_render

before_render(
    ctx: ComponentRenderContext[StateT], area: "Area"
) -> "Area"

Called before rendering; returns the effective render area.

Source code in xnano/components/component.py
def before_render(
    self,
    ctx: ComponentRenderContext[StateT],
    area: "Area",
) -> "Area":
    """Called before rendering; returns the effective render area."""
    return area

after_render

after_render(
    ctx: "ComponentRenderContext[Any]", area: "Area"
) -> None

Record the multi-line editor caret so the terminal cursor tracks it.

Source code in xnano/components/text.py
def after_render(
    self,
    ctx: "ComponentRenderContext[Any]",
    area: "Area",
) -> None:
    """Record the multi-line editor caret so the terminal cursor tracks it."""
    if not (self._input_focused and self._editor is not None):
        self._cursor_position = None
        return
    row, column = self._editor.cursor()
    # ponytail: no soft-wrap/scroll accounting; clamps to the slot, which
    # is exact for a note-sized editor. Add offsets if the body scrolls.
    x = area.x + max(0, min(column, max(0, area.width - 1)))
    y = area.y + max(0, min(row, max(0, area.height - 1)))
    self._cursor_position = (x, y)

compose_extra_small

compose_extra_small(
    ctx: ComponentRenderContext[StateT],
) -> Content | None

Compose content when the viewport is extra small (< 40 cols).

Optional responsive counterpart to :meth:compose. When overridden, it is used instead of compose while the window is in this size tier. Overriding any compose_* variant opts the component into breakpoint dispatch; a component that overrides none pays no per-frame cost.

Source code in xnano/components/component.py
@responsive_noop
def compose_extra_small(
    self, ctx: ComponentRenderContext[StateT]
) -> Content | None:
    """Compose content when the viewport is extra small (< 40 cols).

    Optional responsive counterpart to :meth:`compose`. When
    overridden, it is used *instead of* ``compose`` while the window
    is in this size tier. Overriding any ``compose_*`` variant opts the
    component into breakpoint dispatch; a component that overrides none
    pays no per-frame cost.
    """
    return None

compose_small

compose_small(
    ctx: ComponentRenderContext[StateT],
) -> Content | None

Compose content when the viewport is small (40–79 cols).

See :meth:compose_extra_small.

Source code in xnano/components/component.py
@responsive_noop
def compose_small(
    self, ctx: ComponentRenderContext[StateT]
) -> Content | None:
    """Compose content when the viewport is small (40–79 cols).

    See :meth:`compose_extra_small`.
    """
    return None

compose_medium

compose_medium(
    ctx: ComponentRenderContext[StateT],
) -> Content | None

Compose content when the viewport is medium (80–119 cols).

See :meth:compose_extra_small.

Source code in xnano/components/component.py
@responsive_noop
def compose_medium(
    self, ctx: ComponentRenderContext[StateT]
) -> Content | None:
    """Compose content when the viewport is medium (80–119 cols).

    See :meth:`compose_extra_small`.
    """
    return None

compose_large

compose_large(
    ctx: ComponentRenderContext[StateT],
) -> Content | None

Compose content when the viewport is large (120–159 cols).

See :meth:compose_extra_small.

Source code in xnano/components/component.py
@responsive_noop
def compose_large(
    self, ctx: ComponentRenderContext[StateT]
) -> Content | None:
    """Compose content when the viewport is large (120–159 cols).

    See :meth:`compose_extra_small`.
    """
    return None

compose_extra_large

compose_extra_large(
    ctx: ComponentRenderContext[StateT],
) -> Content | None

Compose content when the viewport is extra large (>= 160 cols).

See :meth:compose_extra_small.

Source code in xnano/components/component.py
@responsive_noop
def compose_extra_large(
    self, ctx: ComponentRenderContext[StateT]
) -> Content | None:
    """Compose content when the viewport is extra large (>= 160 cols).

    See :meth:`compose_extra_small`.
    """
    return None

handle_keyboard

handle_keyboard(keyboard: 'KeyboardEventData') -> bool

Edit this text when it has focus.

Passthrough bindings remain available to hooks. Read-only inputs reject edits, and max_length limits inserted content.

Parameters:

  • keyboard ('KeyboardEventData') –

    The keyboard event payload.

Returns:

  • bool

    True when the key was consumed as text editing.

Source code in xnano/components/text.py
def handle_keyboard(self, keyboard: "KeyboardEventData") -> bool:
    """Edit this text when it has focus.

    Passthrough bindings remain available to hooks. Read-only inputs reject
    edits, and ``max_length`` limits inserted content.

    Args:
        keyboard: The keyboard event payload.

    Returns:
        ``True`` when the key was consumed as text editing.
    """
    if self.passthrough and keyboard.matches(*self.passthrough):
        return False
    if not self.input:
        return False

    if self._editor is not None:
        return self._handle_editor_keyboard(keyboard)
    return self._handle_single_line_keyboard(keyboard)

handle_paste

handle_paste(text: str) -> bool

Insert pasted text at the caret of a multi-line editor.

Parameters:

  • text (str) –

    The pasted clipboard text.

Returns:

  • bool

    True when the paste was consumed by the native editor.

Source code in xnano/components/text.py
def handle_paste(self, text: str) -> bool:
    """Insert pasted text at the caret of a multi-line editor.

    Args:
        text: The pasted clipboard text.

    Returns:
        ``True`` when the paste was consumed by the native editor.
    """
    if self._editor is None:
        return False
    if self.read_only:
        return True
    text = self._clamp_text(text)
    if self.max_length is not None:
        remaining = self.max_length - len(self._editor.text())
        if remaining <= 0:
            return True
        text = text[:remaining]
    self._editor.insert_text(text)
    object.__setattr__(self, "content", self._editor.text())
    return True

get_terminal_node

get_terminal_node(
    ctx: ComponentRenderContext[Any],
) -> TextBlock | Native | Panel | None

Return composed content for terminal compatibility.

Parameters:

Returns:

Source code in xnano/components/text.py
def get_terminal_node(
    self, ctx: ComponentRenderContext[Any]
) -> TextBlock | Native | Panel | None:
    """Return composed content for terminal compatibility.

    Args:
        ctx: Render-time scope for this paint.

    Returns:
        The same interface-neutral content as ``compose``.
    """
    return self.compose(ctx)

load_markdown_source

load_markdown_source(
    source: str | bytes | Path,
) -> tuple[str, Path | None]

Load UTF-8 markdown text and optional base path.

Parameters:

  • source (str | bytes | Path) –

    Filesystem path, literal markdown string, or bytes.

Returns:

  • str

    (text, base_path) where base_path is the parent directory

  • Path | None

    when source was a path.

Source code in xnano/markdown.py
def load_markdown_source(
    source: str | bytes | pathlib.Path,
) -> tuple[str, pathlib.Path | None]:
    """Load UTF-8 markdown text and optional base path.

    Args:
        source: Filesystem path, literal markdown string, or bytes.

    Returns:
        ``(text, base_path)`` where ``base_path`` is the parent directory
        when ``source`` was a path.
    """
    if isinstance(source, pathlib.Path):
        text = source.read_text(encoding="utf-8")
        return text, source.parent
    if isinstance(source, bytes):
        return source.decode("utf-8"), None
    # ``source`` may be literal markdown rather than a path. A long or
    # otherwise invalid document raises ``OSError`` (e.g. "File name too
    # long") or ``ValueError`` (embedded NUL) from the path probe — that
    # just means it is not a filename, so fall through to treating it as
    # text instead of letting the probe crash the caller.
    try:
        path = pathlib.Path(source)
        if path.exists() and path.is_file():
            return path.read_text(encoding="utf-8"), path.parent
    except (OSError, ValueError):
        pass
    return source, None

is_markdown_path

is_markdown_path(path: str | Path) -> bool

Return whether path exists and has a supported Markdown suffix.

Source code in xnano/markdown.py
def is_markdown_path(path: str | pathlib.Path) -> bool:
    """Return whether ``path`` exists and has a supported Markdown suffix."""
    candidate = pathlib.Path(path)
    return (
        candidate.exists()
        and candidate.is_file()
        and candidate.suffix.lower() in _MARKDOWN_SUFFIXES
    )

run_markdown

run_markdown(
    source: str | bytes | Path,
    *,
    terminal: Any | None = None
) -> None

Load and page through a Markdown document interactively.

Uses Terminal.run when a live terminal is available so the document can be scrolled; otherwise renders a single frame.

Parameters:

  • source (str | bytes | Path) –

    Path, literal markdown, or bytes.

  • terminal (Any | None, default: None ) –

    Optional terminal instance to reuse.

Source code in xnano/markdown.py
def run_markdown(
    source: str | bytes | pathlib.Path,
    *,
    terminal: Any | None = None,
) -> None:
    """Load and page through a Markdown document interactively.

    Uses ``Terminal.run`` when a live terminal is available so the
    document can be scrolled; otherwise renders a single frame.

    Args:
        source: Path, literal markdown, or bytes.
        terminal: Optional terminal instance to reuse.
    """
    from xnano.terminal import Terminal

    document = _create_markdown_document(source)
    host = terminal if terminal is not None else Terminal()
    # An explicitly offscreen terminal (``Terminal.offscreen(...)``) must
    # never be run through the live loop — it has no real input source to
    # poll and ``.run()`` would block forever waiting for it. Only fall
    # back to the build-capability check when the caller didn't already
    # tell us which kind of terminal this is.
    if getattr(host, "surface", None) == "offscreen":
        live = False
    else:
        live = Terminal.supports_live_terminal() and sys.stdout.isatty()
    if live and hasattr(host, "run"):
        # Enable the wheel + hover tracking for scroll / image expand.
        try:
            host.device.mouse_capture = True
        except Exception:
            pass
        host.run(document)
    else:
        host.render(document)

render_markdown

render_markdown(
    source: str | bytes | Path,
    *,
    terminal: Any | None = None
) -> "Frame"

Render a Markdown document and return its frame.

Parameters:

  • source (str | bytes | Path) –

    Path, literal markdown, or bytes.

  • terminal (Any | None, default: None ) –

    Optional terminal instance to reuse.

Returns:

  • 'Frame'

    The immutable frame produced by the terminal.

Source code in xnano/markdown.py
def render_markdown(
    source: str | bytes | pathlib.Path,
    *,
    terminal: Any | None = None,
) -> "Frame":
    """Render a Markdown document and return its frame.

    Args:
        source: Path, literal markdown, or bytes.
        terminal: Optional terminal instance to reuse.

    Returns:
        The immutable frame produced by the terminal.
    """
    from xnano.terminal import Terminal

    host = terminal if terminal is not None else Terminal()
    try:
        return host.render(_create_markdown_document(source))
    finally:
        if terminal is None:
            host.close()