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