// 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("\n") buf.WriteString("\n") buf.WriteString("\n") for i, hdr := range t.Header { buf.WriteString("") hdr.PrintHTML(buf) buf.WriteString("\n") } buf.WriteString("\n") buf.WriteString("\n") if len(t.Rows) > 0 { buf.WriteString("\n") for _, row := range t.Rows { buf.WriteString("\n") for i, cell := range row { buf.WriteString("") cell.PrintHTML(buf) buf.WriteString("\n") } buf.WriteString("\n") } buf.WriteString("\n") } buf.WriteString("
\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 "" }