123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- package parser
- import "github.com/gomarkdown/markdown/ast"
- // check if the specified position is preceded by an odd number of backslashes
- func isBackslashEscaped(data []byte, i int) bool {
- backslashes := 0
- for i-backslashes-1 >= 0 && data[i-backslashes-1] == '\\' {
- backslashes++
- }
- return backslashes&1 == 1
- }
- func (p *Parser) tableRow(data []byte, columns []ast.CellAlignFlags, header bool) {
- p.AddBlock(&ast.TableRow{})
- col := 0
- i := skipChar(data, 0, '|')
- n := len(data)
- colspans := 0 // keep track of total colspan in this row.
- for col = 0; col < len(columns) && i < n; col++ {
- colspan := 0
- i = skipChar(data, i, ' ')
- cellStart := i
- // If we are in a codespan we should discount any | we see, check for that here and skip ahead.
- if isCode, _ := codeSpan(p, data[i:], 0); isCode > 0 {
- i += isCode - 1
- }
- for i < n && (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' {
- i++
- }
- cellEnd := i
- // skip the end-of-cell marker, possibly taking us past end of buffer
- // each _extra_ | means a colspan
- for i < len(data) && data[i] == '|' && !isBackslashEscaped(data, i) {
- i++
- colspan++
- }
- // only colspan > 1 make sense.
- if colspan < 2 {
- colspan = 0
- }
- for cellEnd > cellStart && cellEnd-1 < n && data[cellEnd-1] == ' ' {
- cellEnd--
- }
- block := &ast.TableCell{
- IsHeader: header,
- Align: columns[col],
- ColSpan: colspan,
- }
- block.Content = data[cellStart:cellEnd]
- if cellStart == cellEnd && colspans > 0 {
- // an empty cell that we should ignore, it exists because of colspan
- colspans--
- } else {
- p.AddBlock(block)
- }
- if colspan > 0 {
- colspans += colspan - 1
- }
- }
- // pad it out with empty columns to get the right number
- for ; col < len(columns); col++ {
- block := &ast.TableCell{
- IsHeader: header,
- Align: columns[col],
- }
- p.AddBlock(block)
- }
- // silently ignore rows with too many cells
- }
- // tableFooter parses the (optional) table footer.
- func (p *Parser) tableFooter(data []byte) bool {
- colCount := 1
- // ignore up to 3 spaces
- n := len(data)
- i := skipCharN(data, 0, ' ', 3)
- for ; i < n && data[i] != '\n'; i++ {
- // If we are in a codespan we should discount any | we see, check for that here and skip ahead.
- if isCode, _ := codeSpan(p, data[i:], 0); isCode > 0 {
- i += isCode - 1
- }
- if data[i] == '|' && !isBackslashEscaped(data, i) {
- colCount++
- continue
- }
- // remaining data must be the = character
- if data[i] != '=' {
- return false
- }
- }
- // doesn't look like a table footer
- if colCount == 1 {
- return false
- }
- p.AddBlock(&ast.TableFooter{})
- return true
- }
- // tableHeaders parses the header. If recognized it will also add a table.
- func (p *Parser) tableHeader(data []byte, doRender bool) (size int, columns []ast.CellAlignFlags, table ast.Node) {
- i := 0
- colCount := 1
- headerIsUnderline := true
- headerIsWithEmptyFields := true
- for i = 0; i < len(data) && data[i] != '\n'; i++ {
- // If we are in a codespan we should discount any | we see, check for that here and skip ahead.
- if isCode, _ := codeSpan(p, data[i:], 0); isCode > 0 {
- i += isCode - 1
- }
- if data[i] == '|' && !isBackslashEscaped(data, i) {
- colCount++
- }
- if data[i] != '-' && data[i] != ' ' && data[i] != ':' && data[i] != '|' {
- headerIsUnderline = false
- }
- if data[i] != ' ' && data[i] != '|' {
- headerIsWithEmptyFields = false
- }
- }
- // doesn't look like a table header
- if colCount == 1 {
- return
- }
- // include the newline in the data sent to tableRow
- j := skipCharN(data, i, '\n', 1)
- header := data[:j]
- // column count ignores pipes at beginning or end of line
- if data[0] == '|' {
- colCount--
- }
- {
- tmp := header
- // remove whitespace from the end
- for len(tmp) > 0 {
- lastIdx := len(tmp) - 1
- if tmp[lastIdx] == '\n' || tmp[lastIdx] == ' ' {
- tmp = tmp[:lastIdx]
- } else {
- break
- }
- }
- n := len(tmp)
- if n > 2 && tmp[n-1] == '|' && !isBackslashEscaped(tmp, n-1) {
- colCount--
- }
- }
- // if the header looks like a underline, then we omit the header
- // and parse the first line again as underline
- if headerIsUnderline && !headerIsWithEmptyFields {
- header = nil
- i = 0
- } else {
- i++ // move past newline
- }
- columns = make([]ast.CellAlignFlags, colCount)
- // move on to the header underline
- if i >= len(data) {
- return
- }
- if data[i] == '|' && !isBackslashEscaped(data, i) {
- i++
- }
- i = skipChar(data, i, ' ')
- // each column header is of form: / *:?-+:? *|/ with # dashes + # colons >= 3
- // and trailing | optional on last column
- col := 0
- n := len(data)
- for i < n && data[i] != '\n' {
- dashes := 0
- if data[i] == ':' {
- i++
- columns[col] |= ast.TableAlignmentLeft
- dashes++
- }
- for i < n && data[i] == '-' {
- i++
- dashes++
- }
- if i < n && data[i] == ':' {
- i++
- columns[col] |= ast.TableAlignmentRight
- dashes++
- }
- for i < n && data[i] == ' ' {
- i++
- }
- if i == n {
- return
- }
- // end of column test is messy
- switch {
- case dashes < 1:
- // not a valid column
- return
- case data[i] == '|' && !isBackslashEscaped(data, i):
- // marker found, now skip past trailing whitespace
- col++
- i++
- for i < n && data[i] == ' ' {
- i++
- }
- // trailing junk found after last column
- if col >= colCount && i < len(data) && data[i] != '\n' {
- return
- }
- case (data[i] != '|' || isBackslashEscaped(data, i)) && col+1 < colCount:
- // something else found where marker was required
- return
- case data[i] == '\n':
- // marker is optional for the last column
- col++
- default:
- // trailing junk found after last column
- return
- }
- }
- if col != colCount {
- return
- }
- if doRender {
- table = &ast.Table{}
- p.AddBlock(table)
- if header != nil {
- p.AddBlock(&ast.TableHeader{})
- p.tableRow(header, columns, true)
- }
- }
- size = skipCharN(data, i, '\n', 1)
- return
- }
- /*
- Table:
- Name | Age | Phone
- ------|-----|---------
- Bob | 31 | 555-1234
- Alice | 27 | 555-4321
- */
- func (p *Parser) table(data []byte) int {
- i, columns, table := p.tableHeader(data, true)
- if i == 0 {
- return 0
- }
- p.AddBlock(&ast.TableBody{})
- for i < len(data) {
- pipes, rowStart := 0, i
- for ; i < len(data) && data[i] != '\n'; i++ {
- if data[i] == '|' {
- pipes++
- }
- }
- if pipes == 0 {
- i = rowStart
- break
- }
- // include the newline in data sent to tableRow
- i = skipCharN(data, i, '\n', 1)
- if p.tableFooter(data[rowStart:i]) {
- continue
- }
- p.tableRow(data[rowStart:i], columns, false)
- }
- if captionContent, id, consumed := p.caption(data[i:], []byte(captionTable)); consumed > 0 {
- caption := &ast.Caption{}
- p.Inline(caption, captionContent)
- // Some switcheroo to re-insert the parsed table as a child of the captionfigure.
- figure := &ast.CaptionFigure{}
- figure.HeadingID = id
- table2 := &ast.Table{}
- // Retain any block level attributes.
- table2.AsContainer().Attribute = table.AsContainer().Attribute
- children := table.GetChildren()
- ast.RemoveFromTree(table)
- table2.SetChildren(children)
- ast.AppendChild(figure, table2)
- ast.AppendChild(figure, caption)
- p.addChild(figure)
- p.Finalize(figure)
- i += consumed
- }
- return i
- }
|