...

Source file src/github.com/yuin/goldmark/extension/table.go

Documentation: github.com/yuin/goldmark/extension

     1  package extension
     2  
     3  import (
     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  
    18  var escapedPipeCellListKey = parser.NewContextKey()
    19  
    20  type 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.
    27  type TableCellAlignMethod int
    28  
    29  const (
    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.
    48  type 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.
    56  type 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.
    63  func NewTableConfig() TableConfig {
    64  	return TableConfig{
    65  		Config:               html.NewConfig(),
    66  		TableCellAlignMethod: TableCellAlignDefault,
    67  	}
    68  }
    69  
    70  // SetOption implements renderer.SetOptioner.
    71  func (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  
    80  type withTableHTMLOptions struct {
    81  	value []html.Option
    82  }
    83  
    84  func (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  
    92  func (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.
   101  func WithTableHTMLOptions(opts ...html.Option) TableOption {
   102  	return &withTableHTMLOptions{opts}
   103  }
   104  
   105  const optTableCellAlignMethod renderer.OptionName = "TableTableCellAlignMethod"
   106  
   107  type withTableCellAlignMethod struct {
   108  	value TableCellAlignMethod
   109  }
   110  
   111  func (o *withTableCellAlignMethod) SetConfig(c *renderer.Config) {
   112  	c.Options[optTableCellAlignMethod] = o.value
   113  }
   114  
   115  func (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.
   120  func WithTableCellAlignMethod(a TableCellAlignMethod) TableOption {
   121  	return &withTableCellAlignMethod{a}
   122  }
   123  
   124  func 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  
   136  var tableDelimLeft = regexp.MustCompile(`^\s*\:\-+\s*$`)
   137  var tableDelimRight = regexp.MustCompile(`^\s*\-+\:\s*$`)
   138  var tableDelimCenter = regexp.MustCompile(`^\s*\:\-+\:\s*$`)
   139  var tableDelimNone = regexp.MustCompile(`^\s*\-+\s*$`)
   140  
   141  type tableParagraphTransformer struct {
   142  }
   143  
   144  var defaultTableParagraphTransformer = &tableParagraphTransformer{}
   145  
   146  // NewTableParagraphTransformer returns  a new ParagraphTransformer
   147  // that can transform paragraphs into tables.
   148  func NewTableParagraphTransformer() parser.ParagraphTransformer {
   149  	return defaultTableParagraphTransformer
   150  }
   151  
   152  func (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  
   184  func (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  
   248  func (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  
   279  type tableASTTransformer struct {
   280  }
   281  
   282  var defaultTableASTTransformer = &tableASTTransformer{}
   283  
   284  // NewTableASTTransformer returns a parser.ASTTransformer for tables.
   285  func NewTableASTTransformer() parser.ASTTransformer {
   286  	return defaultTableASTTransformer
   287  }
   288  
   289  func (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.
   336  type TableHTMLRenderer struct {
   337  	TableConfig
   338  }
   339  
   340  // NewTableHTMLRenderer returns a new TableHTMLRenderer.
   341  func 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.
   352  func (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.
   360  var 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  
   372  func (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.
   386  var 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  
   394  func (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.
   413  var 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  
   421  func (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.
   438  var 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.
   460  var 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  
   480  func (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  
   528  type table struct {
   529  	options []TableOption
   530  }
   531  
   532  // Table is an extension that allow you to use GFM tables .
   533  var Table = &table{
   534  	options: []TableOption{},
   535  }
   536  
   537  // NewTable returns a new extension with given options.
   538  func NewTable(opts ...TableOption) goldmark.Extender {
   539  	return &table{
   540  		options: opts,
   541  	}
   542  }
   543  
   544  func (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  }
   557  

View as plain text