Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/mistune/plugins/table.py: 94%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

163 statements  

1import re 

2from typing import ( 

3 TYPE_CHECKING, 

4 Any, 

5 Dict, 

6 List, 

7 Match, 

8 Optional, 

9 Tuple, 

10 Union, 

11) 

12 

13if TYPE_CHECKING: 

14 from ..block_parser import BlockParser 

15 from ..core import BaseRenderer, BlockState 

16 from ..markdown import Markdown 

17 

18# https://michelf.ca/projects/php-markdown/extra/#table 

19 

20__all__ = ["table", "table_in_quote", "table_in_list"] 

21 

22 

23TABLE_PATTERN = r"^ {0,3}\|[^\n]*\|[ \t]*(?:\n|$)" 

24NP_TABLE_PATTERN = r"^ {0,3}\S[^\n]*\|[^\n]*(?:\n|$)" 

25 

26ALIGN_CENTER = re.compile(r"^ *:-+: *$") 

27ALIGN_LEFT = re.compile(r"^ *:-+ *$") 

28ALIGN_RIGHT = re.compile(r"^ *-+: *$") 

29ALIGN_NONE = re.compile(r"^ *-+ *$") 

30 

31 

32def parse_table(block: "BlockParser", m: Match[str], state: "BlockState") -> Optional[int]: 

33 pos = m.end() 

34 header = _strip_pipe_table_row(m.group(0)) 

35 if header is None: 

36 return None 

37 

38 align_line = state.get_line(pos) 

39 align = _strip_pipe_table_row(align_line) 

40 if align is None: 

41 return None 

42 

43 thead, aligns = _process_thead(header, align) 

44 if not thead: 

45 return _parse_invalid_pipe_table(state, pos + len(align_line)) 

46 assert aligns is not None 

47 pos += len(align_line) 

48 

49 rows = [] 

50 while pos < state.cursor_max: 

51 line = state.get_line(pos) 

52 text = _strip_pipe_table_row(line) 

53 if text is None: 

54 break 

55 

56 row = _process_row(text, aligns) 

57 if not row: 

58 return _parse_invalid_pipe_table(state, pos + len(line)) 

59 rows.append(row) 

60 pos += len(line) 

61 

62 children = [thead, {"type": "table_body", "children": rows}] 

63 state.append_token({"type": "table", "children": children}) 

64 return pos 

65 

66 

67def parse_nptable(block: "BlockParser", m: Match[str], state: "BlockState") -> Optional[int]: 

68 pos = m.end() 

69 header = _strip_table_line(m.group(0)) 

70 if header is None: 

71 return None 

72 

73 align_line = state.get_line(pos) 

74 align = _strip_table_line(align_line) 

75 if align is None: 

76 return None 

77 

78 thead, aligns = _process_thead(header, align) 

79 if not thead: 

80 return None 

81 assert aligns is not None 

82 pos += len(align_line) 

83 

84 rows = [] 

85 while pos < state.cursor_max: 

86 line = state.get_line(pos) 

87 text = _strip_table_line(line) 

88 if text is None: 

89 break 

90 

91 row = _process_row(text, aligns) 

92 if not row: 

93 return None 

94 rows.append(row) 

95 pos += len(line) 

96 

97 children = [thead, {"type": "table_body", "children": rows}] 

98 state.append_token({"type": "table", "children": children}) 

99 return pos 

100 

101 

102def _process_thead(header: str, align: str) -> Union[Tuple[None, None], Tuple[Dict[str, Any], List[Optional[str]]]]: 

103 headers = _split_table_cells(header) 

104 raw_aligns = _split_table_cells(align) 

105 if len(headers) != len(raw_aligns): 

106 return None, None 

107 

108 aligns: List[Optional[str]] = [] 

109 for v in raw_aligns: 

110 if ALIGN_CENTER.match(v): 

111 aligns.append("center") 

112 elif ALIGN_LEFT.match(v): 

113 aligns.append("left") 

114 elif ALIGN_RIGHT.match(v): 

115 aligns.append("right") 

116 elif ALIGN_NONE.match(v) or not v.strip(): 

117 aligns.append(None) 

118 else: 

119 # a delimiter cell must be dashes (optionally colon-flanked) or empty; 

120 # anything else means this is not a delimiter row, so not a table 

121 return None, None 

122 

123 children: List[Dict[str, Any]] = [ 

124 {"type": "table_cell", "text": text.strip(), "attrs": {"align": aligns[i], "head": True}} 

125 for i, text in enumerate(headers) 

126 ] 

127 thead: Dict[str, Any] = {"type": "table_head", "children": children} 

128 return thead, aligns 

129 

130 

131def _process_row(text: str, aligns: List[Optional[str]]) -> Optional[Dict[str, Any]]: 

132 cells = _split_table_cells(text) 

133 if len(cells) != len(aligns): 

134 return None 

135 

136 children: List[Dict[str, Any]] = [ 

137 {"type": "table_cell", "text": text.strip(), "attrs": {"align": aligns[i], "head": False}} 

138 for i, text in enumerate(cells) 

139 ] 

140 return {"type": "table_row", "children": children} 

141 

142 

143def _strip_pipe_table_row(line: str) -> Optional[str]: 

144 text = line.rstrip("\n").rstrip(" \t") 

145 if not text.startswith("|") and text.startswith((" ", "\t")): 

146 text = text.lstrip(" ") 

147 if not text.startswith("|") or not text.endswith("|"): 

148 return None 

149 return text[1:-1] 

150 

151 

152def _parse_invalid_pipe_table(state: "BlockState", pos: int) -> int: 

153 while pos < state.cursor_max: 

154 line = state.get_line(pos) 

155 if _strip_pipe_table_row(line) is None: 

156 break 

157 pos += len(line) 

158 state.add_paragraph(state.src[state.cursor : pos]) 

159 return pos 

160 

161 

162def _strip_table_line(line: str) -> Optional[str]: 

163 text = line.rstrip("\n").rstrip(" \t") 

164 if not text or "|" not in text: 

165 return None 

166 return text 

167 

168 

169def _split_table_cells(text: str) -> List[str]: 

170 cells = [] 

171 start = 0 

172 pos = 0 

173 while pos < len(text): 

174 if text[pos] == "|" and not _is_escaped_pipe(text, pos): 

175 cells.append(text[start:pos].strip()) 

176 start = pos + 1 

177 pos += 1 

178 cells.append(text[start:].strip()) 

179 return cells 

180 

181 

182def _is_escaped_pipe(text: str, pos: int) -> bool: 

183 backslashes = 0 

184 pos -= 1 

185 while pos >= 0 and text[pos] == "\\": 

186 backslashes += 1 

187 pos -= 1 

188 return backslashes % 2 == 1 

189 

190 

191def render_table(renderer: "BaseRenderer", text: str) -> str: 

192 return "<table>\n" + text + "</table>\n" 

193 

194 

195def render_table_head(renderer: "BaseRenderer", text: str) -> str: 

196 return "<thead>\n<tr>\n" + text + "</tr>\n</thead>\n" 

197 

198 

199def render_table_body(renderer: "BaseRenderer", text: str) -> str: 

200 return "<tbody>\n" + text + "</tbody>\n" 

201 

202 

203def render_table_row(renderer: "BaseRenderer", text: str) -> str: 

204 return "<tr>\n" + text + "</tr>\n" 

205 

206 

207def render_table_cell(renderer: "BaseRenderer", text: str, align: Optional[str] = None, head: bool = False) -> str: 

208 if head: 

209 tag = "th" 

210 else: 

211 tag = "td" 

212 

213 html = " <" + tag 

214 if align: 

215 html += ' style="text-align:' + align + '"' 

216 

217 return html + ">" + text + "</" + tag + ">\n" 

218 

219 

220def table(md: "Markdown") -> None: 

221 """A mistune plugin to support table, spec defined at 

222 https://michelf.ca/projects/php-markdown/extra/#table 

223 

224 Here is an example: 

225 

226 .. code-block:: text 

227 

228 First Header | Second Header 

229 ------------- | ------------- 

230 Content Cell | Content Cell 

231 Content Cell | Content Cell 

232 

233 :param md: Markdown instance 

234 """ 

235 md.block.register("table", TABLE_PATTERN, parse_table, before="paragraph") 

236 md.block.register("nptable", NP_TABLE_PATTERN, parse_nptable, before="paragraph") 

237 

238 if md.renderer and md.renderer.NAME == "html": 

239 md.renderer.register("table", render_table) 

240 md.renderer.register("table_head", render_table_head) 

241 md.renderer.register("table_body", render_table_body) 

242 md.renderer.register("table_row", render_table_row) 

243 md.renderer.register("table_cell", render_table_cell) 

244 

245 

246def table_in_quote(md: "Markdown") -> None: 

247 """Enable table plugin in block quotes.""" 

248 md.block.insert_rule(md.block.block_quote_rules, "table", before="paragraph") 

249 md.block.insert_rule(md.block.block_quote_rules, "nptable", before="paragraph") 

250 

251 

252def table_in_list(md: "Markdown") -> None: 

253 """Enable table plugin in list.""" 

254 md.block.insert_rule(md.block.list_rules, "table", before="paragraph") 

255 md.block.insert_rule(md.block.list_rules, "nptable", before="paragraph")