1package extension
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"regexp"
  7
  8	"github.com/yuin/goldmark"
  9	gast "github.com/yuin/goldmark/ast"
 10	"github.com/yuin/goldmark/extension/ast"
 11	"github.com/yuin/goldmark/parser"
 12	"github.com/yuin/goldmark/renderer"
 13	"github.com/yuin/goldmark/renderer/html"
 14	"github.com/yuin/goldmark/text"
 15	"github.com/yuin/goldmark/util"
 16)
 17
 18var escapedPipeCellListKey = parser.NewContextKey()
 19
 20type escapedPipeCell struct {
 21	Cell        *ast.TableCell
 22	Pos         []int
 23	Transformed bool
 24}
 25
 26// TableCellAlignMethod indicates how are table cells aligned in HTML format.indicates how are table cells aligned in HTML format.
 27type TableCellAlignMethod int
 28
 29const (
 30	// TableCellAlignDefault renders alignments by default method.
 31	// With XHTML, alignments are rendered as an align attribute.
 32	// With HTML5, alignments are rendered as a style attribute.
 33	TableCellAlignDefault TableCellAlignMethod = iota
 34
 35	// TableCellAlignAttribute renders alignments as an align attribute.
 36	TableCellAlignAttribute
 37
 38	// TableCellAlignStyle renders alignments as a style attribute.
 39	TableCellAlignStyle
 40
 41	// TableCellAlignNone does not care about alignments.
 42	// If you using classes or other styles, you can add these attributes
 43	// in an ASTTransformer.
 44	TableCellAlignNone
 45)
 46
 47// TableConfig struct holds options for the extension.
 48type TableConfig struct {
 49	html.Config
 50
 51	// TableCellAlignMethod indicates how are table celss aligned.
 52	TableCellAlignMethod TableCellAlignMethod
 53}
 54
 55// TableOption interface is a functional option interface for the extension.
 56type TableOption interface {
 57	renderer.Option
 58	// SetTableOption sets given option to the extension.
 59	SetTableOption(*TableConfig)
 60}
 61
 62// NewTableConfig returns a new Config with defaults.
 63func NewTableConfig() TableConfig {
 64	return TableConfig{
 65		Config:               html.NewConfig(),
 66		TableCellAlignMethod: TableCellAlignDefault,
 67	}
 68}
 69
 70// SetOption implements renderer.SetOptioner.
 71func (c *TableConfig) SetOption(name renderer.OptionName, value interface{}) {
 72	switch name {
 73	case optTableCellAlignMethod:
 74		c.TableCellAlignMethod = value.(TableCellAlignMethod)
 75	default:
 76		c.Config.SetOption(name, value)
 77	}
 78}
 79
 80type withTableHTMLOptions struct {
 81	value []html.Option
 82}
 83
 84func (o *withTableHTMLOptions) SetConfig(c *renderer.Config) {
 85	if o.value != nil {
 86		for _, v := range o.value {
 87			v.(renderer.Option).SetConfig(c)
 88		}
 89	}
 90}
 91
 92func (o *withTableHTMLOptions) SetTableOption(c *TableConfig) {
 93	if o.value != nil {
 94		for _, v := range o.value {
 95			v.SetHTMLOption(&c.Config)
 96		}
 97	}
 98}
 99
100// WithTableHTMLOptions is functional option that wraps goldmark HTMLRenderer options.
101func WithTableHTMLOptions(opts ...html.Option) TableOption {
102	return &withTableHTMLOptions{opts}
103}
104
105const optTableCellAlignMethod renderer.OptionName = "TableTableCellAlignMethod"
106
107type withTableCellAlignMethod struct {
108	value TableCellAlignMethod
109}
110
111func (o *withTableCellAlignMethod) SetConfig(c *renderer.Config) {
112	c.Options[optTableCellAlignMethod] = o.value
113}
114
115func (o *withTableCellAlignMethod) SetTableOption(c *TableConfig) {
116	c.TableCellAlignMethod = o.value
117}
118
119// WithTableCellAlignMethod is a functional option that indicates how are table cells aligned in HTML format.
120func WithTableCellAlignMethod(a TableCellAlignMethod) TableOption {
121	return &withTableCellAlignMethod{a}
122}
123
124func isTableDelim(bs []byte) bool {
125	if w, _ := util.IndentWidth(bs, 0); w > 3 {
126		return false
127	}
128	for _, b := range bs {
129		if !(util.IsSpace(b) || b == '-' || b == '|' || b == ':') {
130			return false
131		}
132	}
133	return true
134}
135
136var tableDelimLeft = regexp.MustCompile(`^\s*\:\-+\s*$`)
137var tableDelimRight = regexp.MustCompile(`^\s*\-+\:\s*$`)
138var tableDelimCenter = regexp.MustCompile(`^\s*\:\-+\:\s*$`)
139var tableDelimNone = regexp.MustCompile(`^\s*\-+\s*$`)
140
141type tableParagraphTransformer struct {
142}
143
144var defaultTableParagraphTransformer = &tableParagraphTransformer{}
145
146// NewTableParagraphTransformer returns  a new ParagraphTransformer
147// that can transform paragraphs into tables.
148func NewTableParagraphTransformer() parser.ParagraphTransformer {
149	return defaultTableParagraphTransformer
150}
151
152func (b *tableParagraphTransformer) Transform(node *gast.Paragraph, reader text.Reader, pc parser.Context) {
153	lines := node.Lines()
154	if lines.Len() < 2 {
155		return
156	}
157	for i := 1; i < lines.Len(); i++ {
158		alignments := b.parseDelimiter(lines.At(i), reader)
159		if alignments == nil {
160			continue
161		}
162		header := b.parseRow(lines.At(i-1), alignments, true, reader, pc)
163		if header == nil || len(alignments) != header.ChildCount() {
164			return
165		}
166		table := ast.NewTable()
167		table.Alignments = alignments
168		table.AppendChild(table, ast.NewTableHeader(header))
169		for j := i + 1; j < lines.Len(); j++ {
170			table.AppendChild(table, b.parseRow(lines.At(j), alignments, false, reader, pc))
171		}
172		node.Lines().SetSliced(0, i-1)
173		node.Parent().InsertAfter(node.Parent(), node, table)
174		if node.Lines().Len() == 0 {
175			node.Parent().RemoveChild(node.Parent(), node)
176		} else {
177			last := node.Lines().At(i - 2)
178			last.Stop = last.Stop - 1 // trim last newline(\n)
179			node.Lines().Set(i-2, last)
180		}
181	}
182}
183
184func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments []ast.Alignment, isHeader bool, reader text.Reader, pc parser.Context) *ast.TableRow {
185	source := reader.Source()
186	line := segment.Value(source)
187	pos := 0
188	pos += util.TrimLeftSpaceLength(line)
189	limit := len(line)
190	limit -= util.TrimRightSpaceLength(line)
191	row := ast.NewTableRow(alignments)
192	if len(line) > 0 && line[pos] == '|' {
193		pos++
194	}
195	if len(line) > 0 && line[limit-1] == '|' {
196		limit--
197	}
198	i := 0
199	for ; pos < limit; i++ {
200		alignment := ast.AlignNone
201		if i >= len(alignments) {
202			if !isHeader {
203				return row
204			}
205		} else {
206			alignment = alignments[i]
207		}
208
209		var escapedCell *escapedPipeCell
210		node := ast.NewTableCell()
211		node.Alignment = alignment
212		hasBacktick := false
213		closure := pos
214		for ; closure < limit; closure++ {
215			if line[closure] == '`' {
216				hasBacktick = true
217			}
218			if line[closure] == '|' {
219				if closure == 0 || line[closure-1] != '\\' {
220					break
221				} else if hasBacktick {
222					if escapedCell == nil {
223						escapedCell = &escapedPipeCell{node, []int{}, false}
224						escapedList := pc.ComputeIfAbsent(escapedPipeCellListKey,
225							func() interface{} {
226								return []*escapedPipeCell{}
227							}).([]*escapedPipeCell)
228						escapedList = append(escapedList, escapedCell)
229						pc.Set(escapedPipeCellListKey, escapedList)
230					}
231					escapedCell.Pos = append(escapedCell.Pos, segment.Start+closure-1)
232				}
233			}
234		}
235		seg := text.NewSegment(segment.Start+pos, segment.Start+closure)
236		seg = seg.TrimLeftSpace(source)
237		seg = seg.TrimRightSpace(source)
238		node.Lines().Append(seg)
239		row.AppendChild(row, node)
240		pos = closure + 1
241	}
242	for ; i < len(alignments); i++ {
243		row.AppendChild(row, ast.NewTableCell())
244	}
245	return row
246}
247
248func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, reader text.Reader) []ast.Alignment {
249
250	line := segment.Value(reader.Source())
251	if !isTableDelim(line) {
252		return nil
253	}
254	cols := bytes.Split(line, []byte{'|'})
255	if util.IsBlank(cols[0]) {
256		cols = cols[1:]
257	}
258	if len(cols) > 0 && util.IsBlank(cols[len(cols)-1]) {
259		cols = cols[:len(cols)-1]
260	}
261
262	var alignments []ast.Alignment
263	for _, col := range cols {
264		if tableDelimLeft.Match(col) {
265			alignments = append(alignments, ast.AlignLeft)
266		} else if tableDelimRight.Match(col) {
267			alignments = append(alignments, ast.AlignRight)
268		} else if tableDelimCenter.Match(col) {
269			alignments = append(alignments, ast.AlignCenter)
270		} else if tableDelimNone.Match(col) {
271			alignments = append(alignments, ast.AlignNone)
272		} else {
273			return nil
274		}
275	}
276	return alignments
277}
278
279type tableASTTransformer struct {
280}
281
282var defaultTableASTTransformer = &tableASTTransformer{}
283
284// NewTableASTTransformer returns a parser.ASTTransformer for tables.
285func NewTableASTTransformer() parser.ASTTransformer {
286	return defaultTableASTTransformer
287}
288
289func (a *tableASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) {
290	lst := pc.Get(escapedPipeCellListKey)
291	if lst == nil {
292		return
293	}
294	pc.Set(escapedPipeCellListKey, nil)
295	for _, v := range lst.([]*escapedPipeCell) {
296		if v.Transformed {
297			continue
298		}
299		_ = gast.Walk(v.Cell, func(n gast.Node, entering bool) (gast.WalkStatus, error) {
300			if !entering || n.Kind() != gast.KindCodeSpan {
301				return gast.WalkContinue, nil
302			}
303
304			for c := n.FirstChild(); c != nil; {
305				next := c.NextSibling()
306				if c.Kind() != gast.KindText {
307					c = next
308					continue
309				}
310				parent := c.Parent()
311				ts := &c.(*gast.Text).Segment
312				n := c
313				for _, v := range lst.([]*escapedPipeCell) {
314					for _, pos := range v.Pos {
315						if ts.Start <= pos && pos < ts.Stop {
316							segment := n.(*gast.Text).Segment
317							n1 := gast.NewRawTextSegment(segment.WithStop(pos))
318							n2 := gast.NewRawTextSegment(segment.WithStart(pos + 1))
319							parent.InsertAfter(parent, n, n1)
320							parent.InsertAfter(parent, n1, n2)
321							parent.RemoveChild(parent, n)
322							n = n2
323							v.Transformed = true
324						}
325					}
326				}
327				c = next
328			}
329			return gast.WalkContinue, nil
330		})
331	}
332}
333
334// TableHTMLRenderer is a renderer.NodeRenderer implementation that
335// renders Table nodes.
336type TableHTMLRenderer struct {
337	TableConfig
338}
339
340// NewTableHTMLRenderer returns a new TableHTMLRenderer.
341func NewTableHTMLRenderer(opts ...TableOption) renderer.NodeRenderer {
342	r := &TableHTMLRenderer{
343		TableConfig: NewTableConfig(),
344	}
345	for _, opt := range opts {
346		opt.SetTableOption(&r.TableConfig)
347	}
348	return r
349}
350
351// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
352func (r *TableHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
353	reg.Register(ast.KindTable, r.renderTable)
354	reg.Register(ast.KindTableHeader, r.renderTableHeader)
355	reg.Register(ast.KindTableRow, r.renderTableRow)
356	reg.Register(ast.KindTableCell, r.renderTableCell)
357}
358
359// TableAttributeFilter defines attribute names which table elements can have.
360var TableAttributeFilter = html.GlobalAttributeFilter.Extend(
361	[]byte("align"),       // [Deprecated]
362	[]byte("bgcolor"),     // [Deprecated]
363	[]byte("border"),      // [Deprecated]
364	[]byte("cellpadding"), // [Deprecated]
365	[]byte("cellspacing"), // [Deprecated]
366	[]byte("frame"),       // [Deprecated]
367	[]byte("rules"),       // [Deprecated]
368	[]byte("summary"),     // [Deprecated]
369	[]byte("width"),       // [Deprecated]
370)
371
372func (r *TableHTMLRenderer) renderTable(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
373	if entering {
374		_, _ = w.WriteString("<table")
375		if n.Attributes() != nil {
376			html.RenderAttributes(w, n, TableAttributeFilter)
377		}
378		_, _ = w.WriteString(">\n")
379	} else {
380		_, _ = w.WriteString("</table>\n")
381	}
382	return gast.WalkContinue, nil
383}
384
385// TableHeaderAttributeFilter defines attribute names which <thead> elements can have.
386var TableHeaderAttributeFilter = html.GlobalAttributeFilter.Extend(
387	[]byte("align"),   // [Deprecated since HTML4] [Obsolete since HTML5]
388	[]byte("bgcolor"), // [Not Standardized]
389	[]byte("char"),    // [Deprecated since HTML4] [Obsolete since HTML5]
390	[]byte("charoff"), // [Deprecated since HTML4] [Obsolete since HTML5]
391	[]byte("valign"),  // [Deprecated since HTML4] [Obsolete since HTML5]
392)
393
394func (r *TableHTMLRenderer) renderTableHeader(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
395	if entering {
396		_, _ = w.WriteString("<thead")
397		if n.Attributes() != nil {
398			html.RenderAttributes(w, n, TableHeaderAttributeFilter)
399		}
400		_, _ = w.WriteString(">\n")
401		_, _ = w.WriteString("<tr>\n") // Header <tr> has no separate handle
402	} else {
403		_, _ = w.WriteString("</tr>\n")
404		_, _ = w.WriteString("</thead>\n")
405		if n.NextSibling() != nil {
406			_, _ = w.WriteString("<tbody>\n")
407		}
408	}
409	return gast.WalkContinue, nil
410}
411
412// TableRowAttributeFilter defines attribute names which <tr> elements can have.
413var TableRowAttributeFilter = html.GlobalAttributeFilter.Extend(
414	[]byte("align"),   // [Obsolete since HTML5]
415	[]byte("bgcolor"), // [Obsolete since HTML5]
416	[]byte("char"),    // [Obsolete since HTML5]
417	[]byte("charoff"), // [Obsolete since HTML5]
418	[]byte("valign"),  // [Obsolete since HTML5]
419)
420
421func (r *TableHTMLRenderer) renderTableRow(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
422	if entering {
423		_, _ = w.WriteString("<tr")
424		if n.Attributes() != nil {
425			html.RenderAttributes(w, n, TableRowAttributeFilter)
426		}
427		_, _ = w.WriteString(">\n")
428	} else {
429		_, _ = w.WriteString("</tr>\n")
430		if n.Parent().LastChild() == n {
431			_, _ = w.WriteString("</tbody>\n")
432		}
433	}
434	return gast.WalkContinue, nil
435}
436
437// TableThCellAttributeFilter defines attribute names which table <th> cells can have.
438var TableThCellAttributeFilter = html.GlobalAttributeFilter.Extend(
439	[]byte("abbr"), // [OK] Contains a short abbreviated description of the cell's content [NOT OK in <td>]
440
441	[]byte("align"),   // [Obsolete since HTML5]
442	[]byte("axis"),    // [Obsolete since HTML5]
443	[]byte("bgcolor"), // [Not Standardized]
444	[]byte("char"),    // [Obsolete since HTML5]
445	[]byte("charoff"), // [Obsolete since HTML5]
446
447	[]byte("colspan"), // [OK] Number of columns that the cell is to span
448	[]byte("headers"), // [OK] This attribute contains a list of space-separated strings, each corresponding to the id attribute of the <th> elements that apply to this element
449
450	[]byte("height"), // [Deprecated since HTML4] [Obsolete since HTML5]
451
452	[]byte("rowspan"), // [OK] Number of rows that the cell is to span
453	[]byte("scope"),   // [OK] This enumerated attribute defines the cells that the header (defined in the <th>) element relates to [NOT OK in <td>]
454
455	[]byte("valign"), // [Obsolete since HTML5]
456	[]byte("width"),  // [Deprecated since HTML4] [Obsolete since HTML5]
457)
458
459// TableTdCellAttributeFilter defines attribute names which table <td> cells can have.
460var TableTdCellAttributeFilter = html.GlobalAttributeFilter.Extend(
461	[]byte("abbr"),    // [Obsolete since HTML5] [OK in <th>]
462	[]byte("align"),   // [Obsolete since HTML5]
463	[]byte("axis"),    // [Obsolete since HTML5]
464	[]byte("bgcolor"), // [Not Standardized]
465	[]byte("char"),    // [Obsolete since HTML5]
466	[]byte("charoff"), // [Obsolete since HTML5]
467
468	[]byte("colspan"), // [OK] Number of columns that the cell is to span
469	[]byte("headers"), // [OK] This attribute contains a list of space-separated strings, each corresponding to the id attribute of the <th> elements that apply to this element
470
471	[]byte("height"), // [Deprecated since HTML4] [Obsolete since HTML5]
472
473	[]byte("rowspan"), // [OK] Number of rows that the cell is to span
474
475	[]byte("scope"),  // [Obsolete since HTML5] [OK in <th>]
476	[]byte("valign"), // [Obsolete since HTML5]
477	[]byte("width"),  // [Deprecated since HTML4] [Obsolete since HTML5]
478)
479
480func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
481	n := node.(*ast.TableCell)
482	tag := "td"
483	if n.Parent().Kind() == ast.KindTableHeader {
484		tag = "th"
485	}
486	if entering {
487		fmt.Fprintf(w, "<%s", tag)
488		if n.Alignment != ast.AlignNone {
489			amethod := r.TableConfig.TableCellAlignMethod
490			if amethod == TableCellAlignDefault {
491				if r.Config.XHTML {
492					amethod = TableCellAlignAttribute
493				} else {
494					amethod = TableCellAlignStyle
495				}
496			}
497			switch amethod {
498			case TableCellAlignAttribute:
499				if _, ok := n.AttributeString("align"); !ok { // Skip align render if overridden
500					fmt.Fprintf(w, ` align="%s"`, n.Alignment.String())
501				}
502			case TableCellAlignStyle:
503				v, ok := n.AttributeString("style")
504				var cob util.CopyOnWriteBuffer
505				if ok {
506					cob = util.NewCopyOnWriteBuffer(v.([]byte))
507					cob.AppendByte(';')
508				}
509				style := fmt.Sprintf("text-align:%s", n.Alignment.String())
510				cob.AppendString(style)
511				n.SetAttributeString("style", cob.Bytes())
512			}
513		}
514		if n.Attributes() != nil {
515			if tag == "td" {
516				html.RenderAttributes(w, n, TableTdCellAttributeFilter) // <td>
517			} else {
518				html.RenderAttributes(w, n, TableThCellAttributeFilter) // <th>
519			}
520		}
521		_ = w.WriteByte('>')
522	} else {
523		fmt.Fprintf(w, "</%s>\n", tag)
524	}
525	return gast.WalkContinue, nil
526}
527
528type table struct {
529	options []TableOption
530}
531
532// Table is an extension that allow you to use GFM tables .
533var Table = &table{
534	options: []TableOption{},
535}
536
537// NewTable returns a new extension with given options.
538func NewTable(opts ...TableOption) goldmark.Extender {
539	return &table{
540		options: opts,
541	}
542}
543
544func (e *table) Extend(m goldmark.Markdown) {
545	m.Parser().AddOptions(
546		parser.WithParagraphTransformers(
547			util.Prioritized(NewTableParagraphTransformer(), 200),
548		),
549		parser.WithASTTransformers(
550			util.Prioritized(defaultTableASTTransformer, 0),
551		),
552	)
553	m.Renderer().AddOptions(renderer.WithNodeRenderers(
554		util.Prioritized(NewTableHTMLRenderer(e.options...), 500),
555	))
556}