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
27 type TableCellAlignMethod int
28
29 const (
30
31
32
33 TableCellAlignDefault TableCellAlignMethod = iota
34
35
36 TableCellAlignAttribute
37
38
39 TableCellAlignStyle
40
41
42
43
44 TableCellAlignNone
45 )
46
47
48 type TableConfig struct {
49 html.Config
50
51
52 TableCellAlignMethod TableCellAlignMethod
53 }
54
55
56 type TableOption interface {
57 renderer.Option
58
59 SetTableOption(*TableConfig)
60 }
61
62
63 func NewTableConfig() TableConfig {
64 return TableConfig{
65 Config: html.NewConfig(),
66 TableCellAlignMethod: TableCellAlignDefault,
67 }
68 }
69
70
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
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
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
147
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
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
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
335
336 type TableHTMLRenderer struct {
337 TableConfig
338 }
339
340
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
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
360 var TableAttributeFilter = html.GlobalAttributeFilter.Extend(
361 []byte("align"),
362 []byte("bgcolor"),
363 []byte("border"),
364 []byte("cellpadding"),
365 []byte("cellspacing"),
366 []byte("frame"),
367 []byte("rules"),
368 []byte("summary"),
369 []byte("width"),
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
386 var TableHeaderAttributeFilter = html.GlobalAttributeFilter.Extend(
387 []byte("align"),
388 []byte("bgcolor"),
389 []byte("char"),
390 []byte("charoff"),
391 []byte("valign"),
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")
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
413 var TableRowAttributeFilter = html.GlobalAttributeFilter.Extend(
414 []byte("align"),
415 []byte("bgcolor"),
416 []byte("char"),
417 []byte("charoff"),
418 []byte("valign"),
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
438 var TableThCellAttributeFilter = html.GlobalAttributeFilter.Extend(
439 []byte("abbr"),
440
441 []byte("align"),
442 []byte("axis"),
443 []byte("bgcolor"),
444 []byte("char"),
445 []byte("charoff"),
446
447 []byte("colspan"),
448 []byte("headers"),
449
450 []byte("height"),
451
452 []byte("rowspan"),
453 []byte("scope"),
454
455 []byte("valign"),
456 []byte("width"),
457 )
458
459
460 var TableTdCellAttributeFilter = html.GlobalAttributeFilter.Extend(
461 []byte("abbr"),
462 []byte("align"),
463 []byte("axis"),
464 []byte("bgcolor"),
465 []byte("char"),
466 []byte("charoff"),
467
468 []byte("colspan"),
469 []byte("headers"),
470
471 []byte("height"),
472
473 []byte("rowspan"),
474
475 []byte("scope"),
476 []byte("valign"),
477 []byte("width"),
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 {
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)
517 } else {
518 html.RenderAttributes(w, n, TableThCellAttributeFilter)
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
533 var Table = &table{
534 options: []TableOption{},
535 }
536
537
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