import re from typing import ( TYPE_CHECKING, Any, Dict, List, Match, Optional, Tuple, Union, ) from ..helpers import PREVENT_BACKSLASH if TYPE_CHECKING: from ..block_parser import BlockParser from ..core import BaseRenderer, BlockState from ..markdown import Markdown # https://michelf.ca/projects/php-markdown/extra/#table __all__ = ["table", "table_in_quote", "table_in_list"] TABLE_PATTERN = ( r"^ {0,3}\|(?P.+)\|[ \t]*\n" r" {0,3}\|(?P *[-:]+[-| :]*)\|[ \t]*\n" r"(?P(?: {0,3}\|.*\|[ \t]*(?:\n|$))*)\n*" ) NP_TABLE_PATTERN = ( r"^ {0,3}(?P\S.*\|.*)\n" r" {0,3}(?P[-:]+ *\|[-| :]*)\n" r"(?P(?:.*\|.*(?:\n|$))*)\n*" ) TABLE_CELL = re.compile(r"^ {0,3}\|(.+)\|[ \t]*$") CELL_SPLIT = re.compile(r" *" + PREVENT_BACKSLASH + r"\| *") ALIGN_CENTER = re.compile(r"^ *:-+: *$") ALIGN_LEFT = re.compile(r"^ *:-+ *$") ALIGN_RIGHT = re.compile(r"^ *-+: *$") def parse_table(block: "BlockParser", m: Match[str], state: "BlockState") -> Optional[int]: pos = m.end() header = m.group("table_head") align = m.group("table_align") thead, aligns = _process_thead(header, align) if not thead: return None assert aligns is not None rows = [] body = m.group("table_body") for text in body.splitlines(): m2 = TABLE_CELL.match(text) if not m2: # pragma: no cover return None row = _process_row(m2.group(1), aligns) if not row: return None rows.append(row) children = [thead, {"type": "table_body", "children": rows}] state.append_token({"type": "table", "children": children}) return pos def parse_nptable(block: "BlockParser", m: Match[str], state: "BlockState") -> Optional[int]: header = m.group("nptable_head") align = m.group("nptable_align") thead, aligns = _process_thead(header, align) if not thead: return None assert aligns is not None rows = [] body = m.group("nptable_body") for text in body.splitlines(): row = _process_row(text, aligns) if not row: return None rows.append(row) children = [thead, {"type": "table_body", "children": rows}] state.append_token({"type": "table", "children": children}) return m.end() def _process_thead(header: str, align: str) -> Union[Tuple[None, None], Tuple[Dict[str, Any], List[str]]]: headers = CELL_SPLIT.split(header) aligns = CELL_SPLIT.split(align) if len(headers) != len(aligns): return None, None for i, v in enumerate(aligns): if ALIGN_CENTER.match(v): aligns[i] = "center" elif ALIGN_LEFT.match(v): aligns[i] = "left" elif ALIGN_RIGHT.match(v): aligns[i] = "right" else: aligns[i] = None children = [ {"type": "table_cell", "text": text.strip(), "attrs": {"align": aligns[i], "head": True}} for i, text in enumerate(headers) ] thead = {"type": "table_head", "children": children} return thead, aligns def _process_row(text: str, aligns: List[str]) -> Optional[Dict[str, Any]]: cells = CELL_SPLIT.split(text) if len(cells) != len(aligns): return None children = [ {"type": "table_cell", "text": text.strip(), "attrs": {"align": aligns[i], "head": False}} for i, text in enumerate(cells) ] return {"type": "table_row", "children": children} def render_table(renderer: "BaseRenderer", text: str) -> str: return "\n" + text + "
\n" def render_table_head(renderer: "BaseRenderer", text: str) -> str: return "\n\n" + text + "\n\n" def render_table_body(renderer: "BaseRenderer", text: str) -> str: return "\n" + text + "\n" def render_table_row(renderer: "BaseRenderer", text: str) -> str: return "\n" + text + "\n" def render_table_cell(renderer: "BaseRenderer", text: str, align: Optional[str] = None, head: bool = False) -> str: if head: tag = "th" else: tag = "td" html = " <" + tag if align: html += ' style="text-align:' + align + '"' return html + ">" + text + "\n" def table(md: "Markdown") -> None: """A mistune plugin to support table, spec defined at https://michelf.ca/projects/php-markdown/extra/#table Here is an example: .. code-block:: text First Header | Second Header ------------- | ------------- Content Cell | Content Cell Content Cell | Content Cell :param md: Markdown instance """ md.block.register("table", TABLE_PATTERN, parse_table, before="paragraph") md.block.register("nptable", NP_TABLE_PATTERN, parse_nptable, before="paragraph") if md.renderer and md.renderer.NAME == "html": md.renderer.register("table", render_table) md.renderer.register("table_head", render_table_head) md.renderer.register("table_body", render_table_body) md.renderer.register("table_row", render_table_row) md.renderer.register("table_cell", render_table_cell) def table_in_quote(md: "Markdown") -> None: """Enable table plugin in block quotes.""" md.block.insert_rule(md.block.block_quote_rules, "table", before="paragraph") md.block.insert_rule(md.block.block_quote_rules, "nptable", before="paragraph") def table_in_list(md: "Markdown") -> None: """Enable table plugin in list.""" md.block.insert_rule(md.block.list_rules, "table", before="paragraph") md.block.insert_rule(md.block.list_rules, "nptable", before="paragraph")