from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any
import polars as pl
from markdown_it.token import Token
from markdown_it.tree import SyntaxTreeNode
from .dataframe_formatting import format_table_cell
from .report_block import DEFERRED_BLOCK_TOKEN_TYPE, BlockContent, DeferredReportBlock
if TYPE_CHECKING:
from .markdown_parser import MarkdownParser
__all__ = [
"NestedListItem",
"append_tokens",
"block_tokens",
"bold_paragraph_tokens",
"deferred_block_token",
"fence_token",
"frontmatter_token",
"heading_tokens",
"horizontal_rule_token",
"html_block_token",
"line_break_token",
"list_item_tokens",
"list_tokens",
"paragraph_tokens",
"raw_token",
"table_cell_tokens",
"table_tokens",
]
type NestedListItem = str | list[NestedListItem]
def append_tokens(document: SyntaxTreeNode, tokens: Sequence[Token]) -> None:
"""Append a valid token fragment to a report syntax tree."""
fragment = SyntaxTreeNode(list(tokens))
for child in fragment.children:
child.parent = document
document.children.append(child)
def block_tokens(parser: MarkdownParser, content: BlockContent) -> list[Token]:
"""Normalize block content into a token stream, parsing Markdown text."""
if isinstance(content, str):
return parser.parse(content)
if isinstance(content, Token):
return [content]
return list(content)
[docs]
def paragraph_tokens(
parser: MarkdownParser,
content: str,
*,
is_hidden: bool = False,
) -> list[Token]:
"""Build a paragraph token pair containing parsed inline Markdown."""
return [
Token("paragraph_open", "p", 1, block=True, hidden=is_hidden),
parser.parse_inline(content),
Token("paragraph_close", "p", -1, block=True, hidden=is_hidden),
]
[docs]
def bold_paragraph_tokens(parser: MarkdownParser, content: str) -> list[Token]:
"""Build a paragraph whose complete inline content is strong text."""
inline = parser.parse_inline(content)
inline.children = [
Token("strong_open", "strong", 1, markup="**"),
*(inline.children or []),
Token("strong_close", "strong", -1, markup="**"),
]
return [
Token("paragraph_open", "p", 1, block=True),
inline,
Token("paragraph_close", "p", -1, block=True),
]
[docs]
def heading_tokens(parser: MarkdownParser, content: str, level: int) -> list[Token]:
"""Build a heading at a level from one through six.
Raises:
ValueError: if level is outside the Markdown heading range.
"""
if not 1 <= level <= 6:
raise ValueError("Heading level must be between 1 and 6")
markup = "#" * level
return [
Token("heading_open", f"h{level}", 1, markup=markup, block=True),
parser.parse_inline(content),
Token("heading_close", f"h{level}", -1, markup=markup, block=True),
]
[docs]
def list_tokens(
parser: MarkdownParser,
items: Sequence[NestedListItem],
*,
is_ordered: bool,
) -> list[Token]:
"""Build an ordered or unordered list, nesting sublists to any depth.
A list element nests beneath the item that precedes it. Every level carries
the marker chosen by is_ordered, so an ordered list nests ordered sublists.
Raises:
ValueError: if a sublist has no preceding item to nest beneath.
"""
list_type = "ordered_list" if is_ordered else "bullet_list"
list_tag = "ol" if is_ordered else "ul"
marker = "." if is_ordered else "-"
tokens = [Token(f"{list_type}_open", list_tag, 1, markup=marker, block=True)]
has_open_item = False
index = 0
for item in items:
if isinstance(item, list):
if not has_open_item:
raise ValueError("A nested list must follow the item it nests beneath")
tokens.extend(list_tokens(parser, item, is_ordered=is_ordered))
continue
if has_open_item:
tokens.append(Token("list_item_close", "li", -1, markup=marker, block=True))
index += 1
tokens.append(
Token(
"list_item_open",
"li",
1,
markup=marker,
info=str(index) if is_ordered else "",
block=True,
)
)
tokens.extend(paragraph_tokens(parser, item, is_hidden=True))
has_open_item = True
if has_open_item:
tokens.append(Token("list_item_close", "li", -1, markup=marker, block=True))
tokens.append(Token(f"{list_type}_close", list_tag, -1, markup=marker, block=True))
return tokens
[docs]
def list_item_tokens(parser: MarkdownParser, content: str) -> list[Token]:
"""Build one complete unordered-list item."""
return [
Token("list_item_open", "li", 1, markup="-", block=True),
*paragraph_tokens(parser, content, is_hidden=True),
Token("list_item_close", "li", -1, markup="-", block=True),
]
[docs]
def table_tokens(parser: MarkdownParser, dataframe: pl.DataFrame) -> list[Token]:
"""Build a GFM table from every DataFrame column and row."""
if dataframe.width == 0:
return [raw_token("||")]
tokens = [
Token("table_open", "table", 1, block=True),
Token("thead_open", "thead", 1, block=True),
Token("tr_open", "tr", 1, block=True),
]
for column_name in dataframe.columns:
tokens.extend(table_cell_tokens(parser, format_table_cell(column_name), is_header=True))
tokens.extend(
[
Token("tr_close", "tr", -1, block=True),
Token("thead_close", "thead", -1, block=True),
Token("tbody_open", "tbody", 1, block=True),
]
)
for row in dataframe.iter_rows():
tokens.append(Token("tr_open", "tr", 1, block=True))
for cell in row:
tokens.extend(table_cell_tokens(parser, format_table_cell(cell), is_header=False))
tokens.append(Token("tr_close", "tr", -1, block=True))
tokens.extend(
[
Token("tbody_close", "tbody", -1, block=True),
Token("table_close", "table", -1, block=True),
]
)
return tokens
[docs]
def table_cell_tokens(parser: MarkdownParser, content: str, *, is_header: bool) -> list[Token]:
"""Build one table header or body cell with inline Markdown."""
cell_type = "th" if is_header else "td"
return [
Token(f"{cell_type}_open", cell_type, 1, block=True),
parser.parse_inline(content),
Token(f"{cell_type}_close", cell_type, -1, block=True),
]
[docs]
def fence_token(content: str, language: str = "") -> Token:
"""Build a fenced code block, terminating the content with a newline."""
if not content.endswith("\n"):
content = f"{content}\n"
return Token("fence", "code", 0, content=content, markup="```", info=language, block=True)
[docs]
def raw_token(content: str) -> Token:
"""Build content that renders verbatim, bypassing Markdown formatting."""
return Token("report_raw", "", 0, content=content, block=True)
def html_block_token(content: str) -> Token:
"""Build a raw HTML block."""
return Token("html_block", "", 0, content=content, block=True)
def frontmatter_token(content: str) -> Token:
"""Build the YAML metadata block that opens a report."""
return Token("report_frontmatter", "", 0, content=content, block=True)
def line_break_token() -> Token:
"""Build one additional blank line between document blocks."""
return Token("report_line_break", "", 0, block=True)
def horizontal_rule_token() -> Token:
"""Build a thematic break."""
return Token("hr", "hr", 0, markup="---", block=True)
def deferred_block_token(block: DeferredReportBlock) -> Token:
"""Build the placeholder standing in for a block resolved at render time."""
meta: dict[str, Any] = {"block": block}
return Token(DEFERRED_BLOCK_TOKEN_TYPE, "", 0, block=True, meta=meta)