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}