gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

table.go (16894B)


      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 }