123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- # GFM table, https://github.github.com/gfm/#tables-extension-
- from __future__ import annotations
- import re
- from ..common.utils import charStrAt, isStrSpace
- from .state_block import StateBlock
- headerLineRe = re.compile(r"^:?-+:?$")
- enclosingPipesRe = re.compile(r"^\||\|$")
- def getLine(state: StateBlock, line: int) -> str:
- pos = state.bMarks[line] + state.tShift[line]
- maximum = state.eMarks[line]
- # return state.src.substr(pos, max - pos)
- return state.src[pos:maximum]
- def escapedSplit(string: str) -> list[str]:
- result: list[str] = []
- pos = 0
- max = len(string)
- isEscaped = False
- lastPos = 0
- current = ""
- ch = charStrAt(string, pos)
- while pos < max:
- if ch == "|":
- if not isEscaped:
- # pipe separating cells, '|'
- result.append(current + string[lastPos:pos])
- current = ""
- lastPos = pos + 1
- else:
- # escaped pipe, '\|'
- current += string[lastPos : pos - 1]
- lastPos = pos
- isEscaped = ch == "\\"
- pos += 1
- ch = charStrAt(string, pos)
- result.append(current + string[lastPos:])
- return result
- def table(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
- tbodyLines = None
- # should have at least two lines
- if startLine + 2 > endLine:
- return False
- nextLine = startLine + 1
- if state.sCount[nextLine] < state.blkIndent:
- return False
- if state.is_code_block(nextLine):
- return False
- # first character of the second line should be '|', '-', ':',
- # and no other characters are allowed but spaces;
- # basically, this is the equivalent of /^[-:|][-:|\s]*$/ regexp
- pos = state.bMarks[nextLine] + state.tShift[nextLine]
- if pos >= state.eMarks[nextLine]:
- return False
- first_ch = state.src[pos]
- pos += 1
- if first_ch not in ("|", "-", ":"):
- return False
- if pos >= state.eMarks[nextLine]:
- return False
- second_ch = state.src[pos]
- pos += 1
- if second_ch not in ("|", "-", ":") and not isStrSpace(second_ch):
- return False
- # if first character is '-', then second character must not be a space
- # (due to parsing ambiguity with list)
- if first_ch == "-" and isStrSpace(second_ch):
- return False
- while pos < state.eMarks[nextLine]:
- ch = state.src[pos]
- if ch not in ("|", "-", ":") and not isStrSpace(ch):
- return False
- pos += 1
- lineText = getLine(state, startLine + 1)
- columns = lineText.split("|")
- aligns = []
- for i in range(len(columns)):
- t = columns[i].strip()
- if not t:
- # allow empty columns before and after table, but not in between columns;
- # e.g. allow ` |---| `, disallow ` ---||--- `
- if i == 0 or i == len(columns) - 1:
- continue
- else:
- return False
- if not headerLineRe.search(t):
- return False
- if charStrAt(t, len(t) - 1) == ":":
- aligns.append("center" if charStrAt(t, 0) == ":" else "right")
- elif charStrAt(t, 0) == ":":
- aligns.append("left")
- else:
- aligns.append("")
- lineText = getLine(state, startLine).strip()
- if "|" not in lineText:
- return False
- if state.is_code_block(startLine):
- return False
- columns = escapedSplit(lineText)
- if columns and columns[0] == "":
- columns.pop(0)
- if columns and columns[-1] == "":
- columns.pop()
- # header row will define an amount of columns in the entire table,
- # and align row should be exactly the same (the rest of the rows can differ)
- columnCount = len(columns)
- if columnCount == 0 or columnCount != len(aligns):
- return False
- if silent:
- return True
- oldParentType = state.parentType
- state.parentType = "table"
- # use 'blockquote' lists for termination because it's
- # the most similar to tables
- terminatorRules = state.md.block.ruler.getRules("blockquote")
- token = state.push("table_open", "table", 1)
- token.map = tableLines = [startLine, 0]
- token = state.push("thead_open", "thead", 1)
- token.map = [startLine, startLine + 1]
- token = state.push("tr_open", "tr", 1)
- token.map = [startLine, startLine + 1]
- for i in range(len(columns)):
- token = state.push("th_open", "th", 1)
- if aligns[i]:
- token.attrs = {"style": "text-align:" + aligns[i]}
- token = state.push("inline", "", 0)
- # note in markdown-it this map was removed in v12.0.0 however, we keep it,
- # since it is helpful to propagate to children tokens
- token.map = [startLine, startLine + 1]
- token.content = columns[i].strip()
- token.children = []
- token = state.push("th_close", "th", -1)
- token = state.push("tr_close", "tr", -1)
- token = state.push("thead_close", "thead", -1)
- nextLine = startLine + 2
- while nextLine < endLine:
- if state.sCount[nextLine] < state.blkIndent:
- break
- terminate = False
- for i in range(len(terminatorRules)):
- if terminatorRules[i](state, nextLine, endLine, True):
- terminate = True
- break
- if terminate:
- break
- lineText = getLine(state, nextLine).strip()
- if not lineText:
- break
- if state.is_code_block(nextLine):
- break
- columns = escapedSplit(lineText)
- if columns and columns[0] == "":
- columns.pop(0)
- if columns and columns[-1] == "":
- columns.pop()
- if nextLine == startLine + 2:
- token = state.push("tbody_open", "tbody", 1)
- token.map = tbodyLines = [startLine + 2, 0]
- token = state.push("tr_open", "tr", 1)
- token.map = [nextLine, nextLine + 1]
- for i in range(columnCount):
- token = state.push("td_open", "td", 1)
- if aligns[i]:
- token.attrs = {"style": "text-align:" + aligns[i]}
- token = state.push("inline", "", 0)
- # note in markdown-it this map was removed in v12.0.0 however, we keep it,
- # since it is helpful to propagate to children tokens
- token.map = [nextLine, nextLine + 1]
- try:
- token.content = columns[i].strip() if columns[i] else ""
- except IndexError:
- token.content = ""
- token.children = []
- token = state.push("td_close", "td", -1)
- token = state.push("tr_close", "tr", -1)
- nextLine += 1
- if tbodyLines:
- token = state.push("tbody_close", "tbody", -1)
- tbodyLines[1] = nextLine
- token = state.push("table_close", "table", -1)
- tableLines[1] = nextLine
- state.parentType = oldParentType
- state.line = nextLine
- return True
|