Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/markdown_it/rules_block/table.py: 6%

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

165 statements  

1# GFM table, https://github.github.com/gfm/#tables-extension- 

2from __future__ import annotations 

3 

4import re 

5 

6from ..common.utils import charStrAt, isStrSpace 

7from .state_block import StateBlock 

8 

9headerLineRe = re.compile(r"^:?-+:?$") 

10enclosingPipesRe = re.compile(r"^\||\|$") 

11 

12# Limit the amount of empty autocompleted cells in a table, 

13# see https://github.com/markdown-it/markdown-it/issues/1000, 

14# Both pulldown-cmark and commonmark-hs limit the number of cells this way to ~200k. 

15# We set it to 65k, which can expand user input by a factor of x370 

16# (256x256 square is 1.8kB expanded into 650kB). 

17MAX_AUTOCOMPLETED_CELLS = 0x10000 

18 

19 

20def getLine(state: StateBlock, line: int) -> str: 

21 pos = state.bMarks[line] + state.tShift[line] 

22 maximum = state.eMarks[line] 

23 

24 # return state.src.substr(pos, max - pos) 

25 return state.src[pos:maximum] 

26 

27 

28def escapedSplit(string: str) -> list[str]: 

29 result: list[str] = [] 

30 pos = 0 

31 max = len(string) 

32 isEscaped = False 

33 lastPos = 0 

34 current = "" 

35 ch = charStrAt(string, pos) 

36 

37 while pos < max: 

38 if ch == "|": 

39 if not isEscaped: 

40 # pipe separating cells, '|' 

41 result.append(current + string[lastPos:pos]) 

42 current = "" 

43 lastPos = pos + 1 

44 else: 

45 # escaped pipe, '\|' 

46 current += string[lastPos : pos - 1] 

47 lastPos = pos 

48 

49 isEscaped = ch == "\\" 

50 pos += 1 

51 

52 ch = charStrAt(string, pos) 

53 

54 result.append(current + string[lastPos:]) 

55 

56 return result 

57 

58 

59def table(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: 

60 tbodyLines = None 

61 

62 # should have at least two lines 

63 if startLine + 2 > endLine: 

64 return False 

65 

66 nextLine = startLine + 1 

67 

68 if state.sCount[nextLine] < state.blkIndent: 

69 return False 

70 

71 if state.is_code_block(nextLine): 

72 return False 

73 

74 # first character of the second line should be '|', '-', ':', 

75 # and no other characters are allowed but spaces; 

76 # basically, this is the equivalent of /^[-:|][-:|\s]*$/ regexp 

77 

78 pos = state.bMarks[nextLine] + state.tShift[nextLine] 

79 if pos >= state.eMarks[nextLine]: 

80 return False 

81 first_ch = state.src[pos] 

82 pos += 1 

83 if first_ch not in ("|", "-", ":"): 

84 return False 

85 

86 if pos >= state.eMarks[nextLine]: 

87 return False 

88 second_ch = state.src[pos] 

89 pos += 1 

90 if second_ch not in ("|", "-", ":") and not isStrSpace(second_ch): 

91 return False 

92 

93 # if first character is '-', then second character must not be a space 

94 # (due to parsing ambiguity with list) 

95 if first_ch == "-" and isStrSpace(second_ch): 

96 return False 

97 

98 while pos < state.eMarks[nextLine]: 

99 ch = state.src[pos] 

100 

101 if ch not in ("|", "-", ":") and not isStrSpace(ch): 

102 return False 

103 

104 pos += 1 

105 

106 lineText = getLine(state, startLine + 1) 

107 

108 columns = lineText.split("|") 

109 aligns = [] 

110 for i in range(len(columns)): 

111 t = columns[i].strip() 

112 if not t: 

113 # allow empty columns before and after table, but not in between columns; 

114 # e.g. allow ` |---| `, disallow ` ---||--- ` 

115 if i == 0 or i == len(columns) - 1: 

116 continue 

117 else: 

118 return False 

119 

120 if not headerLineRe.search(t): 

121 return False 

122 if charStrAt(t, len(t) - 1) == ":": 

123 aligns.append("center" if charStrAt(t, 0) == ":" else "right") 

124 elif charStrAt(t, 0) == ":": 

125 aligns.append("left") 

126 else: 

127 aligns.append("") 

128 

129 lineText = getLine(state, startLine).strip() 

130 if "|" not in lineText: 

131 return False 

132 if state.is_code_block(startLine): 

133 return False 

134 columns = escapedSplit(lineText) 

135 if columns and columns[0] == "": 

136 columns.pop(0) 

137 if columns and columns[-1] == "": 

138 columns.pop() 

139 

140 # header row will define an amount of columns in the entire table, 

141 # and align row should be exactly the same (the rest of the rows can differ) 

142 columnCount = len(columns) 

143 if columnCount == 0 or columnCount != len(aligns): 

144 return False 

145 

146 if silent: 

147 return True 

148 

149 oldParentType = state.parentType 

150 state.parentType = "table" 

151 

152 # use 'blockquote' lists for termination because it's 

153 # the most similar to tables 

154 terminatorRules = state.md.block.ruler.getRules("blockquote") 

155 

156 token = state.push("table_open", "table", 1) 

157 token.map = tableLines = [startLine, 0] 

158 

159 token = state.push("thead_open", "thead", 1) 

160 token.map = [startLine, startLine + 1] 

161 

162 token = state.push("tr_open", "tr", 1) 

163 token.map = [startLine, startLine + 1] 

164 

165 for i in range(len(columns)): 

166 token = state.push("th_open", "th", 1) 

167 if aligns[i]: 

168 token.attrs = {"style": "text-align:" + aligns[i]} 

169 

170 token = state.push("inline", "", 0) 

171 # note in markdown-it this map was removed in v12.0.0 however, we keep it, 

172 # since it is helpful to propagate to children tokens 

173 token.map = [startLine, startLine + 1] 

174 token.content = columns[i].strip() 

175 token.children = [] 

176 

177 token = state.push("th_close", "th", -1) 

178 

179 token = state.push("tr_close", "tr", -1) 

180 token = state.push("thead_close", "thead", -1) 

181 

182 autocompleted_cells = 0 

183 nextLine = startLine + 2 

184 while nextLine < endLine: 

185 if state.sCount[nextLine] < state.blkIndent: 

186 break 

187 

188 terminate = False 

189 for i in range(len(terminatorRules)): 

190 if terminatorRules[i](state, nextLine, endLine, True): 

191 terminate = True 

192 break 

193 

194 if terminate: 

195 break 

196 lineText = getLine(state, nextLine).strip() 

197 if not lineText: 

198 break 

199 if state.is_code_block(nextLine): 

200 break 

201 columns = escapedSplit(lineText) 

202 if columns and columns[0] == "": 

203 columns.pop(0) 

204 if columns and columns[-1] == "": 

205 columns.pop() 

206 

207 # note: autocomplete count can be negative if user specifies more columns than header, 

208 # but that does not affect intended use (which is limiting expansion) 

209 autocompleted_cells += columnCount - len(columns) 

210 if autocompleted_cells > MAX_AUTOCOMPLETED_CELLS: 

211 break 

212 

213 if nextLine == startLine + 2: 

214 token = state.push("tbody_open", "tbody", 1) 

215 token.map = tbodyLines = [startLine + 2, 0] 

216 

217 token = state.push("tr_open", "tr", 1) 

218 token.map = [nextLine, nextLine + 1] 

219 

220 for i in range(columnCount): 

221 token = state.push("td_open", "td", 1) 

222 if aligns[i]: 

223 token.attrs = {"style": "text-align:" + aligns[i]} 

224 

225 token = state.push("inline", "", 0) 

226 # note in markdown-it this map was removed in v12.0.0 however, we keep it, 

227 # since it is helpful to propagate to children tokens 

228 token.map = [nextLine, nextLine + 1] 

229 try: 

230 token.content = columns[i].strip() if columns[i] else "" 

231 except IndexError: 

232 token.content = "" 

233 token.children = [] 

234 

235 token = state.push("td_close", "td", -1) 

236 

237 token = state.push("tr_close", "tr", -1) 

238 

239 nextLine += 1 

240 

241 if tbodyLines: 

242 token = state.push("tbody_close", "tbody", -1) 

243 tbodyLines[1] = nextLine 

244 

245 token = state.push("table_close", "table", -1) 

246 

247 tableLines[1] = nextLine 

248 state.parentType = oldParentType 

249 state.line = nextLine 

250 return True