table.mjs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. // GFM table, https://github.github.com/gfm/#tables-extension-
  2. import { isSpace } from '../common/utils.mjs'
  3. // Limit the amount of empty autocompleted cells in a table,
  4. // see https://github.com/markdown-it/markdown-it/issues/1000,
  5. //
  6. // Both pulldown-cmark and commonmark-hs limit the number of cells this way to ~200k.
  7. // We set it to 65k, which can expand user input by a factor of x370
  8. // (256x256 square is 1.8kB expanded into 650kB).
  9. const MAX_AUTOCOMPLETED_CELLS = 0x10000
  10. function getLine (state, line) {
  11. const pos = state.bMarks[line] + state.tShift[line]
  12. const max = state.eMarks[line]
  13. return state.src.slice(pos, max)
  14. }
  15. function escapedSplit (str) {
  16. const result = []
  17. const max = str.length
  18. let pos = 0
  19. let ch = str.charCodeAt(pos)
  20. let isEscaped = false
  21. let lastPos = 0
  22. let current = ''
  23. while (pos < max) {
  24. if (ch === 0x7c/* | */) {
  25. if (!isEscaped) {
  26. // pipe separating cells, '|'
  27. result.push(current + str.substring(lastPos, pos))
  28. current = ''
  29. lastPos = pos + 1
  30. } else {
  31. // escaped pipe, '\|'
  32. current += str.substring(lastPos, pos - 1)
  33. lastPos = pos
  34. }
  35. }
  36. isEscaped = (ch === 0x5c/* \ */)
  37. pos++
  38. ch = str.charCodeAt(pos)
  39. }
  40. result.push(current + str.substring(lastPos))
  41. return result
  42. }
  43. export default function table (state, startLine, endLine, silent) {
  44. // should have at least two lines
  45. if (startLine + 2 > endLine) { return false }
  46. let nextLine = startLine + 1
  47. if (state.sCount[nextLine] < state.blkIndent) { return false }
  48. // if it's indented more than 3 spaces, it should be a code block
  49. if (state.sCount[nextLine] - state.blkIndent >= 4) { return false }
  50. // first character of the second line should be '|', '-', ':',
  51. // and no other characters are allowed but spaces;
  52. // basically, this is the equivalent of /^[-:|][-:|\s]*$/ regexp
  53. let pos = state.bMarks[nextLine] + state.tShift[nextLine]
  54. if (pos >= state.eMarks[nextLine]) { return false }
  55. const firstCh = state.src.charCodeAt(pos++)
  56. if (firstCh !== 0x7C/* | */ && firstCh !== 0x2D/* - */ && firstCh !== 0x3A/* : */) { return false }
  57. if (pos >= state.eMarks[nextLine]) { return false }
  58. const secondCh = state.src.charCodeAt(pos++)
  59. if (secondCh !== 0x7C/* | */ && secondCh !== 0x2D/* - */ && secondCh !== 0x3A/* : */ && !isSpace(secondCh)) {
  60. return false
  61. }
  62. // if first character is '-', then second character must not be a space
  63. // (due to parsing ambiguity with list)
  64. if (firstCh === 0x2D/* - */ && isSpace(secondCh)) { return false }
  65. while (pos < state.eMarks[nextLine]) {
  66. const ch = state.src.charCodeAt(pos)
  67. if (ch !== 0x7C/* | */ && ch !== 0x2D/* - */ && ch !== 0x3A/* : */ && !isSpace(ch)) { return false }
  68. pos++
  69. }
  70. let lineText = getLine(state, startLine + 1)
  71. let columns = lineText.split('|')
  72. const aligns = []
  73. for (let i = 0; i < columns.length; i++) {
  74. const t = columns[i].trim()
  75. if (!t) {
  76. // allow empty columns before and after table, but not in between columns;
  77. // e.g. allow ` |---| `, disallow ` ---||--- `
  78. if (i === 0 || i === columns.length - 1) {
  79. continue
  80. } else {
  81. return false
  82. }
  83. }
  84. if (!/^:?-+:?$/.test(t)) { return false }
  85. if (t.charCodeAt(t.length - 1) === 0x3A/* : */) {
  86. aligns.push(t.charCodeAt(0) === 0x3A/* : */ ? 'center' : 'right')
  87. } else if (t.charCodeAt(0) === 0x3A/* : */) {
  88. aligns.push('left')
  89. } else {
  90. aligns.push('')
  91. }
  92. }
  93. lineText = getLine(state, startLine).trim()
  94. if (lineText.indexOf('|') === -1) { return false }
  95. if (state.sCount[startLine] - state.blkIndent >= 4) { return false }
  96. columns = escapedSplit(lineText)
  97. if (columns.length && columns[0] === '') columns.shift()
  98. if (columns.length && columns[columns.length - 1] === '') columns.pop()
  99. // header row will define an amount of columns in the entire table,
  100. // and align row should be exactly the same (the rest of the rows can differ)
  101. const columnCount = columns.length
  102. if (columnCount === 0 || columnCount !== aligns.length) { return false }
  103. if (silent) { return true }
  104. const oldParentType = state.parentType
  105. state.parentType = 'table'
  106. // use 'blockquote' lists for termination because it's
  107. // the most similar to tables
  108. const terminatorRules = state.md.block.ruler.getRules('blockquote')
  109. const token_to = state.push('table_open', 'table', 1)
  110. const tableLines = [startLine, 0]
  111. token_to.map = tableLines
  112. const token_tho = state.push('thead_open', 'thead', 1)
  113. token_tho.map = [startLine, startLine + 1]
  114. const token_htro = state.push('tr_open', 'tr', 1)
  115. token_htro.map = [startLine, startLine + 1]
  116. for (let i = 0; i < columns.length; i++) {
  117. const token_ho = state.push('th_open', 'th', 1)
  118. if (aligns[i]) {
  119. token_ho.attrs = [['style', 'text-align:' + aligns[i]]]
  120. }
  121. const token_il = state.push('inline', '', 0)
  122. token_il.content = columns[i].trim()
  123. token_il.children = []
  124. state.push('th_close', 'th', -1)
  125. }
  126. state.push('tr_close', 'tr', -1)
  127. state.push('thead_close', 'thead', -1)
  128. let tbodyLines
  129. let autocompletedCells = 0
  130. for (nextLine = startLine + 2; nextLine < endLine; nextLine++) {
  131. if (state.sCount[nextLine] < state.blkIndent) { break }
  132. let terminate = false
  133. for (let i = 0, l = terminatorRules.length; i < l; i++) {
  134. if (terminatorRules[i](state, nextLine, endLine, true)) {
  135. terminate = true
  136. break
  137. }
  138. }
  139. if (terminate) { break }
  140. lineText = getLine(state, nextLine).trim()
  141. if (!lineText) { break }
  142. if (state.sCount[nextLine] - state.blkIndent >= 4) { break }
  143. columns = escapedSplit(lineText)
  144. if (columns.length && columns[0] === '') columns.shift()
  145. if (columns.length && columns[columns.length - 1] === '') columns.pop()
  146. // note: autocomplete count can be negative if user specifies more columns than header,
  147. // but that does not affect intended use (which is limiting expansion)
  148. autocompletedCells += columnCount - columns.length
  149. if (autocompletedCells > MAX_AUTOCOMPLETED_CELLS) { break }
  150. if (nextLine === startLine + 2) {
  151. const token_tbo = state.push('tbody_open', 'tbody', 1)
  152. token_tbo.map = tbodyLines = [startLine + 2, 0]
  153. }
  154. const token_tro = state.push('tr_open', 'tr', 1)
  155. token_tro.map = [nextLine, nextLine + 1]
  156. for (let i = 0; i < columnCount; i++) {
  157. const token_tdo = state.push('td_open', 'td', 1)
  158. if (aligns[i]) {
  159. token_tdo.attrs = [['style', 'text-align:' + aligns[i]]]
  160. }
  161. const token_il = state.push('inline', '', 0)
  162. token_il.content = columns[i] ? columns[i].trim() : ''
  163. token_il.children = []
  164. state.push('td_close', 'td', -1)
  165. }
  166. state.push('tr_close', 'tr', -1)
  167. }
  168. if (tbodyLines) {
  169. state.push('tbody_close', 'tbody', -1)
  170. tbodyLines[1] = nextLine
  171. }
  172. state.push('table_close', 'table', -1)
  173. tableLines[1] = nextLine
  174. state.parentType = oldParentType
  175. state.line = nextLine
  176. return true
  177. }