Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/markdown_it/rules_block/table.py: 99%
158 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:07 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:07 +0000
1# GFM table, https://github.github.com/gfm/#tables-extension-
2import re
4from ..common.utils import charCodeAt, isSpace
5from .state_block import StateBlock
7headerLineRe = re.compile(r"^:?-+:?$")
8enclosingPipesRe = re.compile(r"^\||\|$")
11def getLine(state: StateBlock, line: int):
12 pos = state.bMarks[line] + state.tShift[line]
13 maximum = state.eMarks[line]
15 # return state.src.substr(pos, max - pos)
16 return state.src[pos:maximum]
19def escapedSplit(string):
20 result = []
21 pos = 0
22 max = len(string)
23 isEscaped = False
24 lastPos = 0
25 current = ""
26 ch = charCodeAt(string, pos)
28 while pos < max:
29 if ch == 0x7C: # /* | */
30 if not isEscaped:
31 # pipe separating cells, '|'
32 result.append(current + string[lastPos:pos])
33 current = ""
34 lastPos = pos + 1
35 else:
36 # escaped pipe, '\|'
37 current += string[lastPos : pos - 1]
38 lastPos = pos
40 isEscaped = ch == 0x5C # /* \ */
41 pos += 1
43 ch = charCodeAt(string, pos)
45 result.append(current + string[lastPos:])
47 return result
50def table(state: StateBlock, startLine: int, endLine: int, silent: bool):
51 tbodyLines = None
53 # should have at least two lines
54 if startLine + 2 > endLine:
55 return False
57 nextLine = startLine + 1
59 if state.sCount[nextLine] < state.blkIndent:
60 return False
62 # if it's indented more than 3 spaces, it should be a code block
63 if state.sCount[nextLine] - state.blkIndent >= 4:
64 return False
66 # first character of the second line should be '|', '-', ':',
67 # and no other characters are allowed but spaces;
68 # basically, this is the equivalent of /^[-:|][-:|\s]*$/ regexp
70 pos = state.bMarks[nextLine] + state.tShift[nextLine]
71 if pos >= state.eMarks[nextLine]:
72 return False
73 first_ch = state.srcCharCode[pos]
74 pos += 1
75 if first_ch not in {0x7C, 0x2D, 0x3A}: # not in {"|", "-", ":"}
76 return False
78 if pos >= state.eMarks[nextLine]:
79 return False
80 second_ch = state.srcCharCode[pos]
81 pos += 1
82 # not in {"|", "-", ":"} and not space
83 if second_ch not in {0x7C, 0x2D, 0x3A} and not isSpace(second_ch):
84 return False
86 # if first character is '-', then second character must not be a space
87 # (due to parsing ambiguity with list)
88 if first_ch == 0x2D and isSpace(second_ch):
89 return False
91 while pos < state.eMarks[nextLine]:
92 ch = state.srcCharCode[pos]
94 # /* | */ /* - */ /* : */
95 if ch not in {0x7C, 0x2D, 0x3A} and not isSpace(ch):
96 return False
98 pos += 1
100 lineText = getLine(state, startLine + 1)
102 columns = lineText.split("|")
103 aligns = []
104 for i in range(len(columns)):
105 t = columns[i].strip()
106 if not t:
107 # allow empty columns before and after table, but not in between columns;
108 # e.g. allow ` |---| `, disallow ` ---||--- `
109 if i == 0 or i == len(columns) - 1:
110 continue
111 else:
112 return False
114 if not headerLineRe.search(t):
115 return False
116 if charCodeAt(t, len(t) - 1) == 0x3A: # /* : */
117 # /* : */
118 aligns.append("center" if charCodeAt(t, 0) == 0x3A else "right")
119 elif charCodeAt(t, 0) == 0x3A: # /* : */
120 aligns.append("left")
121 else:
122 aligns.append("")
124 lineText = getLine(state, startLine).strip()
125 if "|" not in lineText:
126 return False
127 if state.sCount[startLine] - state.blkIndent >= 4:
128 return False
129 columns = escapedSplit(lineText)
130 if columns and columns[0] == "":
131 columns.pop(0)
132 if columns and columns[-1] == "":
133 columns.pop()
135 # header row will define an amount of columns in the entire table,
136 # and align row should be exactly the same (the rest of the rows can differ)
137 columnCount = len(columns)
138 if columnCount == 0 or columnCount != len(aligns):
139 return False
141 if silent:
142 return True
144 oldParentType = state.parentType
145 state.parentType = "table"
147 # use 'blockquote' lists for termination because it's
148 # the most similar to tables
149 terminatorRules = state.md.block.ruler.getRules("blockquote")
151 token = state.push("table_open", "table", 1)
152 token.map = tableLines = [startLine, 0]
154 token = state.push("thead_open", "thead", 1)
155 token.map = [startLine, startLine + 1]
157 token = state.push("tr_open", "tr", 1)
158 token.map = [startLine, startLine + 1]
160 for i in range(len(columns)):
161 token = state.push("th_open", "th", 1)
162 if aligns[i]:
163 token.attrs = {"style": "text-align:" + aligns[i]}
165 token = state.push("inline", "", 0)
166 # note in markdown-it this map was removed in v12.0.0 however, we keep it,
167 # since it is helpful to propagate to children tokens
168 token.map = [startLine, startLine + 1]
169 token.content = columns[i].strip()
170 token.children = []
172 token = state.push("th_close", "th", -1)
174 token = state.push("tr_close", "tr", -1)
175 token = state.push("thead_close", "thead", -1)
177 nextLine = startLine + 2
178 while nextLine < endLine:
179 if state.sCount[nextLine] < state.blkIndent:
180 break
182 terminate = False
183 for i in range(len(terminatorRules)):
184 if terminatorRules[i](state, nextLine, endLine, True):
185 terminate = True
186 break
188 if terminate:
189 break
190 lineText = getLine(state, nextLine).strip()
191 if not lineText:
192 break
193 if state.sCount[nextLine] - state.blkIndent >= 4:
194 break
195 columns = escapedSplit(lineText)
196 if columns and columns[0] == "":
197 columns.pop(0)
198 if columns and columns[-1] == "":
199 columns.pop()
201 if nextLine == startLine + 2:
202 token = state.push("tbody_open", "tbody", 1)
203 token.map = tbodyLines = [startLine + 2, 0]
205 token = state.push("tr_open", "tr", 1)
206 token.map = [nextLine, nextLine + 1]
208 for i in range(columnCount):
209 token = state.push("td_open", "td", 1)
210 if aligns[i]:
211 token.attrs = {"style": "text-align:" + aligns[i]}
213 token = state.push("inline", "", 0)
214 # note in markdown-it this map was removed in v12.0.0 however, we keep it,
215 # since it is helpful to propagate to children tokens
216 token.map = [nextLine, nextLine + 1]
217 try:
218 token.content = columns[i].strip() if columns[i] else ""
219 except IndexError:
220 token.content = ""
221 token.children = []
223 token = state.push("td_close", "td", -1)
225 token = state.push("tr_close", "tr", -1)
227 nextLine += 1
229 if tbodyLines:
230 token = state.push("tbody_close", "tbody", -1)
231 tbodyLines[1] = nextLine
233 token = state.push("table_close", "table", -1)
235 tableLines[1] = nextLine
236 state.parentType = oldParentType
237 state.line = nextLine
238 return True