blob: 95f41c834c68f850f026916707eca0bb913eec27 [file] [log] [blame]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package markdown
import (
"bytes"
"strings"
)
type tableTrimmed string
func isTableSpace(c byte) bool {
return c == ' ' || c == '\t' || c == '\v' || c == '\f'
}
func tableTrimSpace(s string) string {
i := 0
for i < len(s) && isTableSpace(s[i]) {
i++
}
j := len(s)
for j > i && isTableSpace(s[j-1]) {
j--
}
return s[i:j]
}
func tableTrimOuter(row string) tableTrimmed {
row = tableTrimSpace(row)
if len(row) > 0 && row[0] == '|' {
row = row[1:]
}
if len(row) > 0 && row[len(row)-1] == '|' {
row = row[:len(row)-1]
}
return tableTrimmed(row)
}
func isTableStart(hdr1, delim1 string) bool {
// Scan potential delimiter string, counting columns.
// This happens on every line of text,
// so make it relatively quick - nothing expensive.
col := 0
delim := tableTrimOuter(delim1)
i := 0
for ; ; col++ {
for i < len(delim) && isTableSpace(delim[i]) {
i++
}
if i >= len(delim) {
break
}
if i < len(delim) && delim[i] == ':' {
i++
}
if i >= len(delim) || delim[i] != '-' {
return false
}
i++
for i < len(delim) && delim[i] == '-' {
i++
}
if i < len(delim) && delim[i] == ':' {
i++
}
for i < len(delim) && isTableSpace(delim[i]) {
i++
}
if i < len(delim) && delim[i] == '|' {
i++
}
}
if strings.TrimSpace(hdr1) == "|" {
// https://github.com/github/cmark-gfm/pull/127 and
// https://github.com/github/cmark-gfm/pull/128
// fixed a buffer overread by rejecting | by itself as a table line.
// That seems to violate the spec, but we will play along.
return false
}
return col == tableCount(tableTrimOuter(hdr1))
}
func tableCount(row tableTrimmed) int {
col := 1
prev := byte(0)
for i := 0; i < len(row); i++ {
c := row[i]
if c == '|' && prev != '\\' {
col++
}
prev = c
}
return col
}
type tableBuilder struct {
hdr tableTrimmed
delim tableTrimmed
rows []tableTrimmed
}
func (b *tableBuilder) start(hdr, delim string) {
b.hdr = tableTrimOuter(hdr)
b.delim = tableTrimOuter(delim)
}
func (b *tableBuilder) addRow(row string) {
b.rows = append(b.rows, tableTrimOuter(row))
}
type Table struct {
Position
Header []*Text
Align []string // 'l', 'c', 'r' for left, center, right; 0 for unset
Rows [][]*Text
}
func (t *Table) PrintHTML(buf *bytes.Buffer) {
buf.WriteString("<table>\n")
buf.WriteString("<thead>\n")
buf.WriteString("<tr>\n")
for i, hdr := range t.Header {
buf.WriteString("<th")
if t.Align[i] != "" {
buf.WriteString(" align=\"")
buf.WriteString(t.Align[i])
buf.WriteString("\"")
}
buf.WriteString(">")
hdr.PrintHTML(buf)
buf.WriteString("</th>\n")
}
buf.WriteString("</tr>\n")
buf.WriteString("</thead>\n")
if len(t.Rows) > 0 {
buf.WriteString("<tbody>\n")
for _, row := range t.Rows {
buf.WriteString("<tr>\n")
for i, cell := range row {
buf.WriteString("<td")
if i < len(t.Align) && t.Align[i] != "" {
buf.WriteString(" align=\"")
buf.WriteString(t.Align[i])
buf.WriteString("\"")
}
buf.WriteString(">")
cell.PrintHTML(buf)
buf.WriteString("</td>\n")
}
buf.WriteString("</tr>\n")
}
buf.WriteString("</tbody>\n")
}
buf.WriteString("</table>\n")
}
func (t *Table) printMarkdown(buf *bytes.Buffer, s mdState) {
}
func (b *tableBuilder) build(p buildState) Block {
pos := p.pos()
pos.StartLine-- // builder does not count header
pos.EndLine = pos.StartLine + 1 + len(b.rows)
t := &Table{
Position: pos,
}
width := tableCount(b.hdr)
t.Header = b.parseRow(p, b.hdr, pos.StartLine, width)
t.Align = b.parseAlign(b.delim, width)
t.Rows = make([][]*Text, len(b.rows))
for i, row := range b.rows {
t.Rows[i] = b.parseRow(p, row, pos.StartLine+2+i, width)
}
return t
}
func (b *tableBuilder) parseRow(p buildState, row tableTrimmed, line int, width int) []*Text {
out := make([]*Text, 0, width)
pos := Position{StartLine: line, EndLine: line}
start := 0
unesc := nop
for i := 0; i < len(row); i++ {
c := row[i]
if c == '\\' && i+1 < len(row) && row[i+1] == '|' {
unesc = tableUnescape
i++
continue
}
if c == '|' {
out = append(out, p.newText(pos, unesc(strings.Trim(string(row[start:i]), " \t\v\f"))))
if len(out) == width {
// Extra cells are discarded!
return out
}
start = i + 1
unesc = nop
}
}
out = append(out, p.newText(pos, unesc(strings.Trim(string(row[start:]), " \t\v\f"))))
for len(out) < width {
// Missing cells are considered empty.
out = append(out, p.newText(pos, ""))
}
return out
}
func nop(text string) string {
return text
}
func tableUnescape(text string) string {
out := make([]byte, 0, len(text))
for i := 0; i < len(text); i++ {
c := text[i]
if c == '\\' && i+1 < len(text) && text[i+1] == '|' {
i++
c = '|'
}
out = append(out, c)
}
return string(out)
}
func (b *tableBuilder) parseAlign(delim tableTrimmed, n int) []string {
align := make([]string, 0, tableCount(delim))
start := 0
for i := 0; i < len(delim); i++ {
if delim[i] == '|' {
align = append(align, tableAlign(string(delim[start:i])))
start = i + 1
}
}
align = append(align, tableAlign(string(delim[start:])))
return align
}
func tableAlign(cell string) string {
cell = tableTrimSpace(cell)
l := cell[0] == ':'
r := cell[len(cell)-1] == ':'
switch {
case l && r:
return "center"
case l:
return "left"
case r:
return "right"
}
return ""
}