You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
matterbridge/vendor/github.com/gomarkdown/markdown/parser/block_table.go

329 lines
7.0 KiB
Go

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
}