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 TYPE_CHECKING, Any, Dict, List, Match, Optional, Tuple, Union 

3 

4if TYPE_CHECKING: 

5 from ..block_parser import BlockParser 

6 from ..core import BaseRenderer, BlockState 

7 from ..markdown import Markdown 

8 

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

10 

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

12 

13 

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

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

16 

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

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

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

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

21 

22 

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

24 pos = m.end() 

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

26 if header is None: 

27 return None 

28 

29 align_line = state.get_line(pos) 

30 align = _strip_pipe_table_row(align_line) 

31 if align is None: 

32 return None 

33 

34 thead, aligns = _process_thead(header, align) 

35 if not thead: 

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

37 assert aligns is not None 

38 pos += len(align_line) 

39 

40 rows = [] 

41 while pos < state.cursor_max: 

42 line = state.get_line(pos) 

43 text = _strip_pipe_table_row(line) 

44 if text is None: 

45 break 

46 

47 row = _process_row(text, aligns) 

48 if not row: 

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

50 rows.append(row) 

51 pos += len(line) 

52 

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

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

55 return pos 

56 

57 

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

59 pos = m.end() 

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

61 if header is None: 

62 return None 

63 

64 align_line = state.get_line(pos) 

65 align = _strip_table_line(align_line) 

66 if align is None: 

67 return None 

68 

69 thead, aligns = _process_thead(header, align) 

70 if not thead: 

71 return None 

72 assert aligns is not None 

73 pos += len(align_line) 

74 

75 rows = [] 

76 while pos < state.cursor_max: 

77 line = state.get_line(pos) 

78 text = _strip_table_line(line) 

79 if text is None: 

80 break 

81 

82 row = _process_row(text, aligns) 

83 if not row: 

84 return None 

85 rows.append(row) 

86 pos += len(line) 

87 

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

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

90 return pos 

91 

92 

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

94 headers = _split_table_cells(header) 

95 raw_aligns = _split_table_cells(align) 

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

97 return None, None 

98 

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

100 for v in raw_aligns: 

101 if ALIGN_CENTER.match(v): 

102 aligns.append("center") 

103 elif ALIGN_LEFT.match(v): 

104 aligns.append("left") 

105 elif ALIGN_RIGHT.match(v): 

106 aligns.append("right") 

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

108 aligns.append(None) 

109 else: 

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

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

112 return None, None 

113 

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

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

116 for i, text in enumerate(headers) 

117 ] 

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

119 return thead, aligns 

120 

121 

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

123 cells = _split_table_cells(text) 

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

125 return None 

126 

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

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

129 for i, text in enumerate(cells) 

130 ] 

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

132 

133 

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

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

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

137 text = text.lstrip(" ") 

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

139 return None 

140 return text[1:-1] 

141 

142 

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

144 while pos < state.cursor_max: 

145 line = state.get_line(pos) 

146 if _strip_pipe_table_row(line) is None: 

147 break 

148 pos += len(line) 

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

150 return pos 

151 

152 

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

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

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

156 return None 

157 return text 

158 

159 

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

161 cells = [] 

162 start = 0 

163 pos = 0 

164 while pos < len(text): 

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

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

167 start = pos + 1 

168 pos += 1 

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

170 return cells 

171 

172 

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

174 backslashes = 0 

175 pos -= 1 

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

177 backslashes += 1 

178 pos -= 1 

179 return backslashes % 2 == 1 

180 

181 

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

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

184 

185 

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

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

188 

189 

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

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

192 

193 

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

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

196 

197 

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

199 if head: 

200 tag = "th" 

201 else: 

202 tag = "td" 

203 

204 html = " <" + tag 

205 if align: 

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

207 

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

209 

210 

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

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

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

214 

215 Here is an example: 

216 

217 .. code-block:: text 

218 

219 First Header | Second Header 

220 ------------- | ------------- 

221 Content Cell | Content Cell 

222 Content Cell | Content Cell 

223 

224 :param md: Markdown instance 

225 """ 

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

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

228 

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

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

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

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

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

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

235 

236 

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

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

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

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

241 

242 

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

244 """Enable table plugin in list.""" 

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

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