block_table.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. package parser
  2. import "github.com/gomarkdown/markdown/ast"
  3. // check if the specified position is preceded by an odd number of backslashes
  4. func isBackslashEscaped(data []byte, i int) bool {
  5. backslashes := 0
  6. for i-backslashes-1 >= 0 && data[i-backslashes-1] == '\\' {
  7. backslashes++
  8. }
  9. return backslashes&1 == 1
  10. }
  11. func (p *Parser) tableRow(data []byte, columns []ast.CellAlignFlags, header bool) {
  12. p.AddBlock(&ast.TableRow{})
  13. col := 0
  14. i := skipChar(data, 0, '|')
  15. n := len(data)
  16. colspans := 0 // keep track of total colspan in this row.
  17. for col = 0; col < len(columns) && i < n; col++ {
  18. colspan := 0
  19. i = skipChar(data, i, ' ')
  20. cellStart := i
  21. // If we are in a codespan we should discount any | we see, check for that here and skip ahead.
  22. if isCode, _ := codeSpan(p, data[i:], 0); isCode > 0 {
  23. i += isCode - 1
  24. }
  25. for i < n && (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' {
  26. i++
  27. }
  28. cellEnd := i
  29. // skip the end-of-cell marker, possibly taking us past end of buffer
  30. // each _extra_ | means a colspan
  31. for i < len(data) && data[i] == '|' && !isBackslashEscaped(data, i) {
  32. i++
  33. colspan++
  34. }
  35. // only colspan > 1 make sense.
  36. if colspan < 2 {
  37. colspan = 0
  38. }
  39. for cellEnd > cellStart && cellEnd-1 < n && data[cellEnd-1] == ' ' {
  40. cellEnd--
  41. }
  42. block := &ast.TableCell{
  43. IsHeader: header,
  44. Align: columns[col],
  45. ColSpan: colspan,
  46. }
  47. block.Content = data[cellStart:cellEnd]
  48. if cellStart == cellEnd && colspans > 0 {
  49. // an empty cell that we should ignore, it exists because of colspan
  50. colspans--
  51. } else {
  52. p.AddBlock(block)
  53. }
  54. if colspan > 0 {
  55. colspans += colspan - 1
  56. }
  57. }
  58. // pad it out with empty columns to get the right number
  59. for ; col < len(columns); col++ {
  60. block := &ast.TableCell{
  61. IsHeader: header,
  62. Align: columns[col],
  63. }
  64. p.AddBlock(block)
  65. }
  66. // silently ignore rows with too many cells
  67. }
  68. // tableFooter parses the (optional) table footer.
  69. func (p *Parser) tableFooter(data []byte) bool {
  70. colCount := 1
  71. // ignore up to 3 spaces
  72. n := len(data)
  73. i := skipCharN(data, 0, ' ', 3)
  74. for ; i < n && data[i] != '\n'; i++ {
  75. // If we are in a codespan we should discount any | we see, check for that here and skip ahead.
  76. if isCode, _ := codeSpan(p, data[i:], 0); isCode > 0 {
  77. i += isCode - 1
  78. }
  79. if data[i] == '|' && !isBackslashEscaped(data, i) {
  80. colCount++
  81. continue
  82. }
  83. // remaining data must be the = character
  84. if data[i] != '=' {
  85. return false
  86. }
  87. }
  88. // doesn't look like a table footer
  89. if colCount == 1 {
  90. return false
  91. }
  92. p.AddBlock(&ast.TableFooter{})
  93. return true
  94. }
  95. // tableHeaders parses the header. If recognized it will also add a table.
  96. func (p *Parser) tableHeader(data []byte, doRender bool) (size int, columns []ast.CellAlignFlags, table ast.Node) {
  97. i := 0
  98. colCount := 1
  99. headerIsUnderline := true
  100. headerIsWithEmptyFields := true
  101. for i = 0; i < len(data) && data[i] != '\n'; i++ {
  102. // If we are in a codespan we should discount any | we see, check for that here and skip ahead.
  103. if isCode, _ := codeSpan(p, data[i:], 0); isCode > 0 {
  104. i += isCode - 1
  105. }
  106. if data[i] == '|' && !isBackslashEscaped(data, i) {
  107. colCount++
  108. }
  109. if data[i] != '-' && data[i] != ' ' && data[i] != ':' && data[i] != '|' {
  110. headerIsUnderline = false
  111. }
  112. if data[i] != ' ' && data[i] != '|' {
  113. headerIsWithEmptyFields = false
  114. }
  115. }
  116. // doesn't look like a table header
  117. if colCount == 1 {
  118. return
  119. }
  120. // include the newline in the data sent to tableRow
  121. j := skipCharN(data, i, '\n', 1)
  122. header := data[:j]
  123. // column count ignores pipes at beginning or end of line
  124. if data[0] == '|' {
  125. colCount--
  126. }
  127. {
  128. tmp := header
  129. // remove whitespace from the end
  130. for len(tmp) > 0 {
  131. lastIdx := len(tmp) - 1
  132. if tmp[lastIdx] == '\n' || tmp[lastIdx] == ' ' {
  133. tmp = tmp[:lastIdx]
  134. } else {
  135. break
  136. }
  137. }
  138. n := len(tmp)
  139. if n > 2 && tmp[n-1] == '|' && !isBackslashEscaped(tmp, n-1) {
  140. colCount--
  141. }
  142. }
  143. // if the header looks like a underline, then we omit the header
  144. // and parse the first line again as underline
  145. if headerIsUnderline && !headerIsWithEmptyFields {
  146. header = nil
  147. i = 0
  148. } else {
  149. i++ // move past newline
  150. }
  151. columns = make([]ast.CellAlignFlags, colCount)
  152. // move on to the header underline
  153. if i >= len(data) {
  154. return
  155. }
  156. if data[i] == '|' && !isBackslashEscaped(data, i) {
  157. i++
  158. }
  159. i = skipChar(data, i, ' ')
  160. // each column header is of form: / *:?-+:? *|/ with # dashes + # colons >= 3
  161. // and trailing | optional on last column
  162. col := 0
  163. n := len(data)
  164. for i < n && data[i] != '\n' {
  165. dashes := 0
  166. if data[i] == ':' {
  167. i++
  168. columns[col] |= ast.TableAlignmentLeft
  169. dashes++
  170. }
  171. for i < n && data[i] == '-' {
  172. i++
  173. dashes++
  174. }
  175. if i < n && data[i] == ':' {
  176. i++
  177. columns[col] |= ast.TableAlignmentRight
  178. dashes++
  179. }
  180. for i < n && data[i] == ' ' {
  181. i++
  182. }
  183. if i == n {
  184. return
  185. }
  186. // end of column test is messy
  187. switch {
  188. case dashes < 1:
  189. // not a valid column
  190. return
  191. case data[i] == '|' && !isBackslashEscaped(data, i):
  192. // marker found, now skip past trailing whitespace
  193. col++
  194. i++
  195. for i < n && data[i] == ' ' {
  196. i++
  197. }
  198. // trailing junk found after last column
  199. if col >= colCount && i < len(data) && data[i] != '\n' {
  200. return
  201. }
  202. case (data[i] != '|' || isBackslashEscaped(data, i)) && col+1 < colCount:
  203. // something else found where marker was required
  204. return
  205. case data[i] == '\n':
  206. // marker is optional for the last column
  207. col++
  208. default:
  209. // trailing junk found after last column
  210. return
  211. }
  212. }
  213. if col != colCount {
  214. return
  215. }
  216. if doRender {
  217. table = &ast.Table{}
  218. p.AddBlock(table)
  219. if header != nil {
  220. p.AddBlock(&ast.TableHeader{})
  221. p.tableRow(header, columns, true)
  222. }
  223. }
  224. size = skipCharN(data, i, '\n', 1)
  225. return
  226. }
  227. /*
  228. Table:
  229. Name | Age | Phone
  230. ------|-----|---------
  231. Bob | 31 | 555-1234
  232. Alice | 27 | 555-4321
  233. */
  234. func (p *Parser) table(data []byte) int {
  235. i, columns, table := p.tableHeader(data, true)
  236. if i == 0 {
  237. return 0
  238. }
  239. p.AddBlock(&ast.TableBody{})
  240. for i < len(data) {
  241. pipes, rowStart := 0, i
  242. for ; i < len(data) && data[i] != '\n'; i++ {
  243. if data[i] == '|' {
  244. pipes++
  245. }
  246. }
  247. if pipes == 0 {
  248. i = rowStart
  249. break
  250. }
  251. // include the newline in data sent to tableRow
  252. i = skipCharN(data, i, '\n', 1)
  253. if p.tableFooter(data[rowStart:i]) {
  254. continue
  255. }
  256. p.tableRow(data[rowStart:i], columns, false)
  257. }
  258. if captionContent, id, consumed := p.caption(data[i:], []byte(captionTable)); consumed > 0 {
  259. caption := &ast.Caption{}
  260. p.Inline(caption, captionContent)
  261. // Some switcheroo to re-insert the parsed table as a child of the captionfigure.
  262. figure := &ast.CaptionFigure{}
  263. figure.HeadingID = id
  264. table2 := &ast.Table{}
  265. // Retain any block level attributes.
  266. table2.AsContainer().Attribute = table.AsContainer().Attribute
  267. children := table.GetChildren()
  268. ast.RemoveFromTree(table)
  269. table2.SetChildren(children)
  270. ast.AppendChild(figure, table2)
  271. ast.AppendChild(figure, caption)
  272. p.addChild(figure)
  273. p.Finalize(figure)
  274. i += consumed
  275. }
  276. return i
  277. }