...

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.
    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,
   185  	alignments []ast.Alignment, isHeader bool, reader text.Reader, pc parser.Context) *ast.TableRow {
   186  	source := reader.Source()
   187  	line := segment.Value(source)
   188  	pos := 0
   189  	pos += util.TrimLeftSpaceLength(line)
   190  	limit := len(line)
   191  	limit -= util.TrimRightSpaceLength(line)
   192  	row := ast.NewTableRow(alignments)
   193  	if len(line) > 0 && line[pos] == '|' {
   194  		pos++
   195  	}
   196  	if len(line) > 0 && line[limit-1] == '|' {
   197  		limit--
   198  	}
   199  	i := 0
   200  	for ; pos < limit; i++ {
   201  		alignment := ast.AlignNone
   202  		if i >= len(alignments) {
   203  			if !isHeader {
   204  				return row
   205  			}
   206  		} else {
   207  			alignment = alignments[i]
   208  		}
   209  
   210  		var escapedCell *escapedPipeCell
   211  		node := ast.NewTableCell()
   212  		node.Alignment = alignment
   213  		hasBacktick := false
   214  		closure := pos
   215  		for ; closure < limit; closure++ {
   216  			if line[closure] == '`' {
   217  				hasBacktick = true
   218  			}
   219  			if line[closure] == '|' {
   220  				if closure == 0 || line[closure-1] != '\\' {
   221  					break
   222  				} else if hasBacktick {
   223  					if escapedCell == nil {
   224  						escapedCell = &escapedPipeCell{node, []int{}, false}
   225  						escapedList := pc.ComputeIfAbsent(escapedPipeCellListKey,
   226  							func() interface{} {
   227  								return []*escapedPipeCell{}
   228  							}).([]*escapedPipeCell)
   229  						escapedList = append(escapedList, escapedCell)
   230  						pc.Set(escapedPipeCellListKey, escapedList)
   231  					}
   232  					escapedCell.Pos = append(escapedCell.Pos, segment.Start+closure-1)
   233  				}
   234  			}
   235  		}
   236  		seg := text.NewSegment(segment.Start+pos, segment.Start+closure)
   237  		seg = seg.TrimLeftSpace(source)
   238  		seg = seg.TrimRightSpace(source)
   239  		node.Lines().Append(seg)
   240  		row.AppendChild(row, node)
   241  		pos = closure + 1
   242  	}
   243  	for ; i < len(alignments); i++ {
   244  		row.AppendChild(row, ast.NewTableCell())
   245  	}
   246  	return row
   247  }
   248  
   249  func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, reader text.Reader) []ast.Alignment {
   250  
   251  	line := segment.Value(reader.Source())
   252  	if !isTableDelim(line) {
   253  		return nil
   254  	}
   255  	cols := bytes.Split(line, []byte{'|'})
   256  	if util.IsBlank(cols[0]) {
   257  		cols = cols[1:]
   258  	}
   259  	if len(cols) > 0 && util.IsBlank(cols[len(cols)-1]) {
   260  		cols = cols[:len(cols)-1]
   261  	}
   262  
   263  	var alignments []ast.Alignment
   264  	for _, col := range cols {
   265  		if tableDelimLeft.Match(col) {
   266  			alignments = append(alignments, ast.AlignLeft)
   267  		} else if tableDelimRight.Match(col) {
   268  			alignments = append(alignments, ast.AlignRight)
   269  		} else if tableDelimCenter.Match(col) {
   270  			alignments = append(alignments, ast.AlignCenter)
   271  		} else if tableDelimNone.Match(col) {
   272  			alignments = append(alignments, ast.AlignNone)
   273  		} else {
   274  			return nil
   275  		}
   276  	}
   277  	return alignments
   278  }
   279  
   280  type tableASTTransformer struct {
   281  }
   282  
   283  var defaultTableASTTransformer = &tableASTTransformer{}
   284  
   285  // NewTableASTTransformer returns a parser.ASTTransformer for tables.
   286  func NewTableASTTransformer() parser.ASTTransformer {
   287  	return defaultTableASTTransformer
   288  }
   289  
   290  func (a *tableASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) {
   291  	lst := pc.Get(escapedPipeCellListKey)
   292  	if lst == nil {
   293  		return
   294  	}
   295  	pc.Set(escapedPipeCellListKey, nil)
   296  	for _, v := range lst.([]*escapedPipeCell) {
   297  		if v.Transformed {
   298  			continue
   299  		}
   300  		_ = gast.Walk(v.Cell, func(n gast.Node, entering bool) (gast.WalkStatus, error) {
   301  			if !entering || n.Kind() != gast.KindCodeSpan {
   302  				return gast.WalkContinue, nil
   303  			}
   304  
   305  			for c := n.FirstChild(); c != nil; {
   306  				next := c.NextSibling()
   307  				if c.Kind() != gast.KindText {
   308  					c = next
   309  					continue
   310  				}
   311  				parent := c.Parent()
   312  				ts := &c.(*gast.Text).Segment
   313  				n := c
   314  				for _, v := range lst.([]*escapedPipeCell) {
   315  					for _, pos := range v.Pos {
   316  						if ts.Start <= pos && pos < ts.Stop {
   317  							segment := n.(*gast.Text).Segment
   318  							n1 := gast.NewRawTextSegment(segment.WithStop(pos))
   319  							n2 := gast.NewRawTextSegment(segment.WithStart(pos + 1))
   320  							parent.InsertAfter(parent, n, n1)
   321  							parent.InsertAfter(parent, n1, n2)
   322  							parent.RemoveChild(parent, n)
   323  							n = n2
   324  							v.Transformed = true
   325  						}
   326  					}
   327  				}
   328  				c = next
   329  			}
   330  			return gast.WalkContinue, nil
   331  		})
   332  	}
   333  }
   334  
   335  // TableHTMLRenderer is a renderer.NodeRenderer implementation that
   336  // renders Table nodes.
   337  type TableHTMLRenderer struct {
   338  	TableConfig
   339  }
   340  
   341  // NewTableHTMLRenderer returns a new TableHTMLRenderer.
   342  func NewTableHTMLRenderer(opts ...TableOption) renderer.NodeRenderer {
   343  	r := &TableHTMLRenderer{
   344  		TableConfig: NewTableConfig(),
   345  	}
   346  	for _, opt := range opts {
   347  		opt.SetTableOption(&r.TableConfig)
   348  	}
   349  	return r
   350  }
   351  
   352  // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
   353  func (r *TableHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
   354  	reg.Register(ast.KindTable, r.renderTable)
   355  	reg.Register(ast.KindTableHeader, r.renderTableHeader)
   356  	reg.Register(ast.KindTableRow, r.renderTableRow)
   357  	reg.Register(ast.KindTableCell, r.renderTableCell)
   358  }
   359  
   360  // TableAttributeFilter defines attribute names which table elements can have.
   361  var TableAttributeFilter = html.GlobalAttributeFilter.Extend(
   362  	[]byte("align"),       // [Deprecated]
   363  	[]byte("bgcolor"),     // [Deprecated]
   364  	[]byte("border"),      // [Deprecated]
   365  	[]byte("cellpadding"), // [Deprecated]
   366  	[]byte("cellspacing"), // [Deprecated]
   367  	[]byte("frame"),       // [Deprecated]
   368  	[]byte("rules"),       // [Deprecated]
   369  	[]byte("summary"),     // [Deprecated]
   370  	[]byte("width"),       // [Deprecated]
   371  )
   372  
   373  func (r *TableHTMLRenderer) renderTable(
   374  	w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
   375  	if entering {
   376  		_, _ = w.WriteString("<table")
   377  		if n.Attributes() != nil {
   378  			html.RenderAttributes(w, n, TableAttributeFilter)
   379  		}
   380  		_, _ = w.WriteString(">\n")
   381  	} else {
   382  		_, _ = w.WriteString("</table>\n")
   383  	}
   384  	return gast.WalkContinue, nil
   385  }
   386  
   387  // TableHeaderAttributeFilter defines attribute names which <thead> elements can have.
   388  var TableHeaderAttributeFilter = html.GlobalAttributeFilter.Extend(
   389  	[]byte("align"),   // [Deprecated since HTML4] [Obsolete since HTML5]
   390  	[]byte("bgcolor"), // [Not Standardized]
   391  	[]byte("char"),    // [Deprecated since HTML4] [Obsolete since HTML5]
   392  	[]byte("charoff"), // [Deprecated since HTML4] [Obsolete since HTML5]
   393  	[]byte("valign"),  // [Deprecated since HTML4] [Obsolete since HTML5]
   394  )
   395  
   396  func (r *TableHTMLRenderer) renderTableHeader(
   397  	w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
   398  	if entering {
   399  		_, _ = w.WriteString("<thead")
   400  		if n.Attributes() != nil {
   401  			html.RenderAttributes(w, n, TableHeaderAttributeFilter)
   402  		}
   403  		_, _ = w.WriteString(">\n")
   404  		_, _ = w.WriteString("<tr>\n") // Header <tr> has no separate handle
   405  	} else {
   406  		_, _ = w.WriteString("</tr>\n")
   407  		_, _ = w.WriteString("</thead>\n")
   408  		if n.NextSibling() != nil {
   409  			_, _ = w.WriteString("<tbody>\n")
   410  		}
   411  	}
   412  	return gast.WalkContinue, nil
   413  }
   414  
   415  // TableRowAttributeFilter defines attribute names which <tr> elements can have.
   416  var TableRowAttributeFilter = html.GlobalAttributeFilter.Extend(
   417  	[]byte("align"),   // [Obsolete since HTML5]
   418  	[]byte("bgcolor"), // [Obsolete since HTML5]
   419  	[]byte("char"),    // [Obsolete since HTML5]
   420  	[]byte("charoff"), // [Obsolete since HTML5]
   421  	[]byte("valign"),  // [Obsolete since HTML5]
   422  )
   423  
   424  func (r *TableHTMLRenderer) renderTableRow(
   425  	w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
   426  	if entering {
   427  		_, _ = w.WriteString("<tr")
   428  		if n.Attributes() != nil {
   429  			html.RenderAttributes(w, n, TableRowAttributeFilter)
   430  		}
   431  		_, _ = w.WriteString(">\n")
   432  	} else {
   433  		_, _ = w.WriteString("</tr>\n")
   434  		if n.Parent().LastChild() == n {
   435  			_, _ = w.WriteString("</tbody>\n")
   436  		}
   437  	}
   438  	return gast.WalkContinue, nil
   439  }
   440  
   441  // TableThCellAttributeFilter defines attribute names which table <th> cells can have.
   442  var TableThCellAttributeFilter = html.GlobalAttributeFilter.Extend(
   443  	[]byte("abbr"), // [OK] Contains a short abbreviated description of the cell's content [NOT OK in <td>]
   444  
   445  	[]byte("align"),   // [Obsolete since HTML5]
   446  	[]byte("axis"),    // [Obsolete since HTML5]
   447  	[]byte("bgcolor"), // [Not Standardized]
   448  	[]byte("char"),    // [Obsolete since HTML5]
   449  	[]byte("charoff"), // [Obsolete since HTML5]
   450  
   451  	[]byte("colspan"), // [OK] Number of columns that the cell is to span
   452  	[]byte("headers"), // [OK] This attribute contains a list of space-separated
   453  	// strings, each corresponding to the id attribute of the <th> elements that apply to this element
   454  
   455  	[]byte("height"), // [Deprecated since HTML4] [Obsolete since HTML5]
   456  
   457  	[]byte("rowspan"), // [OK] Number of rows that the cell is to span
   458  	[]byte("scope"),   // [OK] This enumerated attribute defines the cells that
   459  	// the header (defined in the <th>) element relates to [NOT OK in <td>]
   460  
   461  	[]byte("valign"), // [Obsolete since HTML5]
   462  	[]byte("width"),  // [Deprecated since HTML4] [Obsolete since HTML5]
   463  )
   464  
   465  // TableTdCellAttributeFilter defines attribute names which table <td> cells can have.
   466  var TableTdCellAttributeFilter = html.GlobalAttributeFilter.Extend(
   467  	[]byte("abbr"),    // [Obsolete since HTML5] [OK in <th>]
   468  	[]byte("align"),   // [Obsolete since HTML5]
   469  	[]byte("axis"),    // [Obsolete since HTML5]
   470  	[]byte("bgcolor"), // [Not Standardized]
   471  	[]byte("char"),    // [Obsolete since HTML5]
   472  	[]byte("charoff"), // [Obsolete since HTML5]
   473  
   474  	[]byte("colspan"), // [OK] Number of columns that the cell is to span
   475  	[]byte("headers"), // [OK] This attribute contains a list of space-separated
   476  	// strings, each corresponding to the id attribute of the <th> elements that apply to this element
   477  
   478  	[]byte("height"), // [Deprecated since HTML4] [Obsolete since HTML5]
   479  
   480  	[]byte("rowspan"), // [OK] Number of rows that the cell is to span
   481  
   482  	[]byte("scope"),  // [Obsolete since HTML5] [OK in <th>]
   483  	[]byte("valign"), // [Obsolete since HTML5]
   484  	[]byte("width"),  // [Deprecated since HTML4] [Obsolete since HTML5]
   485  )
   486  
   487  func (r *TableHTMLRenderer) renderTableCell(
   488  	w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
   489  	n := node.(*ast.TableCell)
   490  	tag := "td"
   491  	if n.Parent().Kind() == ast.KindTableHeader {
   492  		tag = "th"
   493  	}
   494  	if entering {
   495  		fmt.Fprintf(w, "<%s", tag)
   496  		if n.Alignment != ast.AlignNone {
   497  			amethod := r.TableConfig.TableCellAlignMethod
   498  			if amethod == TableCellAlignDefault {
   499  				if r.Config.XHTML {
   500  					amethod = TableCellAlignAttribute
   501  				} else {
   502  					amethod = TableCellAlignStyle
   503  				}
   504  			}
   505  			switch amethod {
   506  			case TableCellAlignAttribute:
   507  				if _, ok := n.AttributeString("align"); !ok { // Skip align render if overridden
   508  					fmt.Fprintf(w, ` align="%s"`, n.Alignment.String())
   509  				}
   510  			case TableCellAlignStyle:
   511  				v, ok := n.AttributeString("style")
   512  				var cob util.CopyOnWriteBuffer
   513  				if ok {
   514  					cob = util.NewCopyOnWriteBuffer(v.([]byte))
   515  					cob.AppendByte(';')
   516  				}
   517  				style := fmt.Sprintf("text-align:%s", n.Alignment.String())
   518  				cob.AppendString(style)
   519  				n.SetAttributeString("style", cob.Bytes())
   520  			}
   521  		}
   522  		if n.Attributes() != nil {
   523  			if tag == "td" {
   524  				html.RenderAttributes(w, n, TableTdCellAttributeFilter) // <td>
   525  			} else {
   526  				html.RenderAttributes(w, n, TableThCellAttributeFilter) // <th>
   527  			}
   528  		}
   529  		_ = w.WriteByte('>')
   530  	} else {
   531  		fmt.Fprintf(w, "</%s>\n", tag)
   532  	}
   533  	return gast.WalkContinue, nil
   534  }
   535  
   536  type table struct {
   537  	options []TableOption
   538  }
   539  
   540  // Table is an extension that allow you to use GFM tables .
   541  var Table = &table{
   542  	options: []TableOption{},
   543  }
   544  
   545  // NewTable returns a new extension with given options.
   546  func NewTable(opts ...TableOption) goldmark.Extender {
   547  	return &table{
   548  		options: opts,
   549  	}
   550  }
   551  
   552  func (e *table) Extend(m goldmark.Markdown) {
   553  	m.Parser().AddOptions(
   554  		parser.WithParagraphTransformers(
   555  			util.Prioritized(NewTableParagraphTransformer(), 200),
   556  		),
   557  		parser.WithASTTransformers(
   558  			util.Prioritized(defaultTableASTTransformer, 0),
   559  		),
   560  	)
   561  	m.Renderer().AddOptions(renderer.WithNodeRenderers(
   562  		util.Prioritized(NewTableHTMLRenderer(e.options...), 500),
   563  	))
   564  }
   565  

View as plain text