1import re
2from typing import (
3 TYPE_CHECKING,
4 Any,
5 Dict,
6 List,
7 Match,
8 Optional,
9 Tuple,
10 Union,
11)
12
13from ..helpers import PREVENT_BACKSLASH
14
15if TYPE_CHECKING:
16 from ..block_parser import BlockParser
17 from ..core import BaseRenderer, BlockState
18 from ..markdown import Markdown
19
20# https://michelf.ca/projects/php-markdown/extra/#table
21
22__all__ = ["table", "table_in_quote", "table_in_list"]
23
24
25TABLE_PATTERN = (
26 r"^ {0,3}\|(?P<table_head>.+)\|[ \t]*\n"
27 r" {0,3}\|(?P<table_align> *[-:]+[-| :]*)\|[ \t]*\n"
28 r"(?P<table_body>(?: {0,3}\|.*\|[ \t]*(?:\n|$))*)\n*"
29)
30NP_TABLE_PATTERN = (
31 r"^ {0,3}(?P<nptable_head>\S.*\|.*)\n"
32 r" {0,3}(?P<nptable_align>[-:]+ *\|[-| :]*)\n"
33 r"(?P<nptable_body>(?:.*\|.*(?:\n|$))*)\n*"
34)
35
36TABLE_CELL = re.compile(r"^ {0,3}\|(.+)\|[ \t]*$")
37CELL_SPLIT = re.compile(r" *" + PREVENT_BACKSLASH + r"\| *")
38ALIGN_CENTER = re.compile(r"^ *:-+: *$")
39ALIGN_LEFT = re.compile(r"^ *:-+ *$")
40ALIGN_RIGHT = re.compile(r"^ *-+: *$")
41
42
43def parse_table(block: "BlockParser", m: Match[str], state: "BlockState") -> Optional[int]:
44 pos = m.end()
45 header = m.group("table_head")
46 align = m.group("table_align")
47 thead, aligns = _process_thead(header, align)
48 if not thead:
49 return None
50 assert aligns is not None
51
52 rows = []
53 body = m.group("table_body")
54 for text in body.splitlines():
55 m2 = TABLE_CELL.match(text)
56 if not m2: # pragma: no cover
57 return None
58 row = _process_row(m2.group(1), aligns)
59 if not row:
60 return None
61 rows.append(row)
62
63 children = [thead, {"type": "table_body", "children": rows}]
64 state.append_token({"type": "table", "children": children})
65 return pos
66
67
68def parse_nptable(block: "BlockParser", m: Match[str], state: "BlockState") -> Optional[int]:
69 header = m.group("nptable_head")
70 align = m.group("nptable_align")
71 thead, aligns = _process_thead(header, align)
72 if not thead:
73 return None
74 assert aligns is not None
75
76 rows = []
77 body = m.group("nptable_body")
78 for text in body.splitlines():
79 row = _process_row(text, aligns)
80 if not row:
81 return None
82 rows.append(row)
83
84 children = [thead, {"type": "table_body", "children": rows}]
85 state.append_token({"type": "table", "children": children})
86 return m.end()
87
88
89def _process_thead(header: str, align: str) -> Union[Tuple[None, None], Tuple[Dict[str, Any], List[str]]]:
90 headers = CELL_SPLIT.split(header)
91 aligns = CELL_SPLIT.split(align)
92 if len(headers) != len(aligns):
93 return None, None
94
95 for i, v in enumerate(aligns):
96 if ALIGN_CENTER.match(v):
97 aligns[i] = "center"
98 elif ALIGN_LEFT.match(v):
99 aligns[i] = "left"
100 elif ALIGN_RIGHT.match(v):
101 aligns[i] = "right"
102 else:
103 aligns[i] = None
104
105 children = [
106 {"type": "table_cell", "text": text.strip(), "attrs": {"align": aligns[i], "head": True}}
107 for i, text in enumerate(headers)
108 ]
109 thead = {"type": "table_head", "children": children}
110 return thead, aligns
111
112
113def _process_row(text: str, aligns: List[str]) -> Optional[Dict[str, Any]]:
114 cells = CELL_SPLIT.split(text)
115 if len(cells) != len(aligns):
116 return None
117
118 children = [
119 {"type": "table_cell", "text": text.strip(), "attrs": {"align": aligns[i], "head": False}}
120 for i, text in enumerate(cells)
121 ]
122 return {"type": "table_row", "children": children}
123
124
125def render_table(renderer: "BaseRenderer", text: str) -> str:
126 return "<table>\n" + text + "</table>\n"
127
128
129def render_table_head(renderer: "BaseRenderer", text: str) -> str:
130 return "<thead>\n<tr>\n" + text + "</tr>\n</thead>\n"
131
132
133def render_table_body(renderer: "BaseRenderer", text: str) -> str:
134 return "<tbody>\n" + text + "</tbody>\n"
135
136
137def render_table_row(renderer: "BaseRenderer", text: str) -> str:
138 return "<tr>\n" + text + "</tr>\n"
139
140
141def render_table_cell(renderer: "BaseRenderer", text: str, align: Optional[str] = None, head: bool = False) -> str:
142 if head:
143 tag = "th"
144 else:
145 tag = "td"
146
147 html = " <" + tag
148 if align:
149 html += ' style="text-align:' + align + '"'
150
151 return html + ">" + text + "</" + tag + ">\n"
152
153
154def table(md: "Markdown") -> None:
155 """A mistune plugin to support table, spec defined at
156 https://michelf.ca/projects/php-markdown/extra/#table
157
158 Here is an example:
159
160 .. code-block:: text
161
162 First Header | Second Header
163 ------------- | -------------
164 Content Cell | Content Cell
165 Content Cell | Content Cell
166
167 :param md: Markdown instance
168 """
169 md.block.register("table", TABLE_PATTERN, parse_table, before="paragraph")
170 md.block.register("nptable", NP_TABLE_PATTERN, parse_nptable, before="paragraph")
171
172 if md.renderer and md.renderer.NAME == "html":
173 md.renderer.register("table", render_table)
174 md.renderer.register("table_head", render_table_head)
175 md.renderer.register("table_body", render_table_body)
176 md.renderer.register("table_row", render_table_row)
177 md.renderer.register("table_cell", render_table_cell)
178
179
180def table_in_quote(md: "Markdown") -> None:
181 """Enable table plugin in block quotes."""
182 md.block.insert_rule(md.block.block_quote_rules, "table", before="paragraph")
183 md.block.insert_rule(md.block.block_quote_rules, "nptable", before="paragraph")
184
185
186def table_in_list(md: "Markdown") -> None:
187 """Enable table plugin in list."""
188 md.block.insert_rule(md.block.list_rules, "table", before="paragraph")
189 md.block.insert_rule(md.block.list_rules, "nptable", before="paragraph")