package extension
import (
"bytes"
"fmt"
"regexp"
"github.com/yuin/goldmark"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
var escapedPipeCellListKey = parser .NewContextKey ()
type escapedPipeCell struct {
Cell *ast .TableCell
Pos []int
Transformed bool
}
type TableCellAlignMethod int
const (
TableCellAlignDefault TableCellAlignMethod = iota
TableCellAlignAttribute
TableCellAlignStyle
TableCellAlignNone
)
type TableConfig struct {
html .Config
TableCellAlignMethod TableCellAlignMethod
}
type TableOption interface {
renderer .Option
SetTableOption (*TableConfig )
}
func NewTableConfig () TableConfig {
return TableConfig {
Config : html .NewConfig (),
TableCellAlignMethod : TableCellAlignDefault ,
}
}
func (c *TableConfig ) SetOption (name renderer .OptionName , value interface {}) {
switch name {
case optTableCellAlignMethod :
c .TableCellAlignMethod = value .(TableCellAlignMethod )
default :
c .Config .SetOption (name , value )
}
}
type withTableHTMLOptions struct {
value []html .Option
}
func (o *withTableHTMLOptions ) SetConfig (c *renderer .Config ) {
if o .value != nil {
for _ , v := range o .value {
v .(renderer .Option ).SetConfig (c )
}
}
}
func (o *withTableHTMLOptions ) SetTableOption (c *TableConfig ) {
if o .value != nil {
for _ , v := range o .value {
v .SetHTMLOption (&c .Config )
}
}
}
func WithTableHTMLOptions (opts ...html .Option ) TableOption {
return &withTableHTMLOptions {opts }
}
const optTableCellAlignMethod renderer .OptionName = "TableTableCellAlignMethod"
type withTableCellAlignMethod struct {
value TableCellAlignMethod
}
func (o *withTableCellAlignMethod ) SetConfig (c *renderer .Config ) {
c .Options [optTableCellAlignMethod ] = o .value
}
func (o *withTableCellAlignMethod ) SetTableOption (c *TableConfig ) {
c .TableCellAlignMethod = o .value
}
func WithTableCellAlignMethod (a TableCellAlignMethod ) TableOption {
return &withTableCellAlignMethod {a }
}
func isTableDelim(bs []byte ) bool {
if w , _ := util .IndentWidth (bs , 0 ); w > 3 {
return false
}
for _ , b := range bs {
if !(util .IsSpace (b ) || b == '-' || b == '|' || b == ':' ) {
return false
}
}
return true
}
var tableDelimLeft = regexp .MustCompile (`^\s*\:\-+\s*$` )
var tableDelimRight = regexp .MustCompile (`^\s*\-+\:\s*$` )
var tableDelimCenter = regexp .MustCompile (`^\s*\:\-+\:\s*$` )
var tableDelimNone = regexp .MustCompile (`^\s*\-+\s*$` )
type tableParagraphTransformer struct {
}
var defaultTableParagraphTransformer = &tableParagraphTransformer {}
func NewTableParagraphTransformer () parser .ParagraphTransformer {
return defaultTableParagraphTransformer
}
func (b *tableParagraphTransformer ) Transform (node *gast .Paragraph , reader text .Reader , pc parser .Context ) {
lines := node .Lines ()
if lines .Len () < 2 {
return
}
for i := 1 ; i < lines .Len (); i ++ {
alignments := b .parseDelimiter (lines .At (i ), reader )
if alignments == nil {
continue
}
header := b .parseRow (lines .At (i -1 ), alignments , true , reader , pc )
if header == nil || len (alignments ) != header .ChildCount () {
return
}
table := ast .NewTable ()
table .Alignments = alignments
table .AppendChild (table , ast .NewTableHeader (header ))
for j := i + 1 ; j < lines .Len (); j ++ {
table .AppendChild (table , b .parseRow (lines .At (j ), alignments , false , reader , pc ))
}
node .Lines ().SetSliced (0 , i -1 )
node .Parent ().InsertAfter (node .Parent (), node , table )
if node .Lines ().Len () == 0 {
node .Parent ().RemoveChild (node .Parent (), node )
} else {
last := node .Lines ().At (i - 2 )
last .Stop = last .Stop - 1
node .Lines ().Set (i -2 , last )
}
}
}
func (b *tableParagraphTransformer ) parseRow (segment text .Segment ,
alignments []ast .Alignment , isHeader bool , reader text .Reader , pc parser .Context ) *ast .TableRow {
source := reader .Source ()
line := segment .Value (source )
pos := 0
pos += util .TrimLeftSpaceLength (line )
limit := len (line )
limit -= util .TrimRightSpaceLength (line )
row := ast .NewTableRow (alignments )
if len (line ) > 0 && line [pos ] == '|' {
pos ++
}
if len (line ) > 0 && line [limit -1 ] == '|' {
limit --
}
i := 0
for ; pos < limit ; i ++ {
alignment := ast .AlignNone
if i >= len (alignments ) {
if !isHeader {
return row
}
} else {
alignment = alignments [i ]
}
var escapedCell *escapedPipeCell
node := ast .NewTableCell ()
node .Alignment = alignment
hasBacktick := false
closure := pos
for ; closure < limit ; closure ++ {
if line [closure ] == '`' {
hasBacktick = true
}
if line [closure ] == '|' {
if closure == 0 || line [closure -1 ] != '\\' {
break
} else if hasBacktick {
if escapedCell == nil {
escapedCell = &escapedPipeCell {node , []int {}, false }
escapedList := pc .ComputeIfAbsent (escapedPipeCellListKey ,
func () interface {} {
return []*escapedPipeCell {}
}).([]*escapedPipeCell )
escapedList = append (escapedList , escapedCell )
pc .Set (escapedPipeCellListKey , escapedList )
}
escapedCell .Pos = append (escapedCell .Pos , segment .Start +closure -1 )
}
}
}
seg := text .NewSegment (segment .Start +pos , segment .Start +closure )
seg = seg .TrimLeftSpace (source )
seg = seg .TrimRightSpace (source )
node .Lines ().Append (seg )
row .AppendChild (row , node )
pos = closure + 1
}
for ; i < len (alignments ); i ++ {
row .AppendChild (row , ast .NewTableCell ())
}
return row
}
func (b *tableParagraphTransformer ) parseDelimiter (segment text .Segment , reader text .Reader ) []ast .Alignment {
line := segment .Value (reader .Source ())
if !isTableDelim (line ) {
return nil
}
cols := bytes .Split (line , []byte {'|' })
if util .IsBlank (cols [0 ]) {
cols = cols [1 :]
}
if len (cols ) > 0 && util .IsBlank (cols [len (cols )-1 ]) {
cols = cols [:len (cols )-1 ]
}
var alignments []ast .Alignment
for _ , col := range cols {
if tableDelimLeft .Match (col ) {
alignments = append (alignments , ast .AlignLeft )
} else if tableDelimRight .Match (col ) {
alignments = append (alignments , ast .AlignRight )
} else if tableDelimCenter .Match (col ) {
alignments = append (alignments , ast .AlignCenter )
} else if tableDelimNone .Match (col ) {
alignments = append (alignments , ast .AlignNone )
} else {
return nil
}
}
return alignments
}
type tableASTTransformer struct {
}
var defaultTableASTTransformer = &tableASTTransformer {}
func NewTableASTTransformer () parser .ASTTransformer {
return defaultTableASTTransformer
}
func (a *tableASTTransformer ) Transform (node *gast .Document , reader text .Reader , pc parser .Context ) {
lst := pc .Get (escapedPipeCellListKey )
if lst == nil {
return
}
pc .Set (escapedPipeCellListKey , nil )
for _ , v := range lst .([]*escapedPipeCell ) {
if v .Transformed {
continue
}
_ = gast .Walk (v .Cell , func (n gast .Node , entering bool ) (gast .WalkStatus , error ) {
if !entering || n .Kind () != gast .KindCodeSpan {
return gast .WalkContinue , nil
}
for c := n .FirstChild (); c != nil ; {
next := c .NextSibling ()
if c .Kind () != gast .KindText {
c = next
continue
}
parent := c .Parent ()
ts := &c .(*gast .Text ).Segment
n := c
for _ , v := range lst .([]*escapedPipeCell ) {
for _ , pos := range v .Pos {
if ts .Start <= pos && pos < ts .Stop {
segment := n .(*gast .Text ).Segment
n1 := gast .NewRawTextSegment (segment .WithStop (pos ))
n2 := gast .NewRawTextSegment (segment .WithStart (pos + 1 ))
parent .InsertAfter (parent , n , n1 )
parent .InsertAfter (parent , n1 , n2 )
parent .RemoveChild (parent , n )
n = n2
v .Transformed = true
}
}
}
c = next
}
return gast .WalkContinue , nil
})
}
}
type TableHTMLRenderer struct {
TableConfig
}
func NewTableHTMLRenderer (opts ...TableOption ) renderer .NodeRenderer {
r := &TableHTMLRenderer {
TableConfig : NewTableConfig (),
}
for _ , opt := range opts {
opt .SetTableOption (&r .TableConfig )
}
return r
}
func (r *TableHTMLRenderer ) RegisterFuncs (reg renderer .NodeRendererFuncRegisterer ) {
reg .Register (ast .KindTable , r .renderTable )
reg .Register (ast .KindTableHeader , r .renderTableHeader )
reg .Register (ast .KindTableRow , r .renderTableRow )
reg .Register (ast .KindTableCell , r .renderTableCell )
}
var TableAttributeFilter = html .GlobalAttributeFilter .Extend (
[]byte ("align" ),
[]byte ("bgcolor" ),
[]byte ("border" ),
[]byte ("cellpadding" ),
[]byte ("cellspacing" ),
[]byte ("frame" ),
[]byte ("rules" ),
[]byte ("summary" ),
[]byte ("width" ),
)
func (r *TableHTMLRenderer ) renderTable (
w util .BufWriter , source []byte , n gast .Node , entering bool ) (gast .WalkStatus , error ) {
if entering {
_, _ = w .WriteString ("<table" )
if n .Attributes () != nil {
html .RenderAttributes (w , n , TableAttributeFilter )
}
_, _ = w .WriteString (">\n" )
} else {
_, _ = w .WriteString ("</table>\n" )
}
return gast .WalkContinue , nil
}
var TableHeaderAttributeFilter = html .GlobalAttributeFilter .Extend (
[]byte ("align" ),
[]byte ("bgcolor" ),
[]byte ("char" ),
[]byte ("charoff" ),
[]byte ("valign" ),
)
func (r *TableHTMLRenderer ) renderTableHeader (
w util .BufWriter , source []byte , n gast .Node , entering bool ) (gast .WalkStatus , error ) {
if entering {
_, _ = w .WriteString ("<thead" )
if n .Attributes () != nil {
html .RenderAttributes (w , n , TableHeaderAttributeFilter )
}
_, _ = w .WriteString (">\n" )
_, _ = w .WriteString ("<tr>\n" )
} else {
_, _ = w .WriteString ("</tr>\n" )
_, _ = w .WriteString ("</thead>\n" )
if n .NextSibling () != nil {
_, _ = w .WriteString ("<tbody>\n" )
}
}
return gast .WalkContinue , nil
}
var TableRowAttributeFilter = html .GlobalAttributeFilter .Extend (
[]byte ("align" ),
[]byte ("bgcolor" ),
[]byte ("char" ),
[]byte ("charoff" ),
[]byte ("valign" ),
)
func (r *TableHTMLRenderer ) renderTableRow (
w util .BufWriter , source []byte , n gast .Node , entering bool ) (gast .WalkStatus , error ) {
if entering {
_, _ = w .WriteString ("<tr" )
if n .Attributes () != nil {
html .RenderAttributes (w , n , TableRowAttributeFilter )
}
_, _ = w .WriteString (">\n" )
} else {
_, _ = w .WriteString ("</tr>\n" )
if n .Parent ().LastChild () == n {
_, _ = w .WriteString ("</tbody>\n" )
}
}
return gast .WalkContinue , nil
}
var TableThCellAttributeFilter = html .GlobalAttributeFilter .Extend (
[]byte ("abbr" ),
[]byte ("align" ),
[]byte ("axis" ),
[]byte ("bgcolor" ),
[]byte ("char" ),
[]byte ("charoff" ),
[]byte ("colspan" ),
[]byte ("headers" ),
[]byte ("height" ),
[]byte ("rowspan" ),
[]byte ("scope" ),
[]byte ("valign" ),
[]byte ("width" ),
)
var TableTdCellAttributeFilter = html .GlobalAttributeFilter .Extend (
[]byte ("abbr" ),
[]byte ("align" ),
[]byte ("axis" ),
[]byte ("bgcolor" ),
[]byte ("char" ),
[]byte ("charoff" ),
[]byte ("colspan" ),
[]byte ("headers" ),
[]byte ("height" ),
[]byte ("rowspan" ),
[]byte ("scope" ),
[]byte ("valign" ),
[]byte ("width" ),
)
func (r *TableHTMLRenderer ) renderTableCell (
w util .BufWriter , source []byte , node gast .Node , entering bool ) (gast .WalkStatus , error ) {
n := node .(*ast .TableCell )
tag := "td"
if n .Parent ().Kind () == ast .KindTableHeader {
tag = "th"
}
if entering {
_, _ = fmt .Fprintf (w , "<%s" , tag )
if n .Alignment != ast .AlignNone {
amethod := r .TableConfig .TableCellAlignMethod
if amethod == TableCellAlignDefault {
if r .Config .XHTML {
amethod = TableCellAlignAttribute
} else {
amethod = TableCellAlignStyle
}
}
switch amethod {
case TableCellAlignAttribute :
if _ , ok := n .AttributeString ("align" ); !ok {
_, _ = fmt .Fprintf (w , ` align="%s"` , n .Alignment .String ())
}
case TableCellAlignStyle :
v , ok := n .AttributeString ("style" )
var cob util .CopyOnWriteBuffer
if ok {
cob = util .NewCopyOnWriteBuffer (v .([]byte ))
cob .AppendByte (';' )
}
style := fmt .Sprintf ("text-align:%s" , n .Alignment .String ())
cob .AppendString (style )
n .SetAttributeString ("style" , cob .Bytes ())
}
}
if n .Attributes () != nil {
if tag == "td" {
html .RenderAttributes (w , n , TableTdCellAttributeFilter )
} else {
html .RenderAttributes (w , n , TableThCellAttributeFilter )
}
}
_ = w .WriteByte ('>' )
} else {
_, _ = fmt .Fprintf (w , "</%s>\n" , tag )
}
return gast .WalkContinue , nil
}
type table struct {
options []TableOption
}
var Table = &table {
options : []TableOption {},
}
func NewTable (opts ...TableOption ) goldmark .Extender {
return &table {
options : opts ,
}
}
func (e *table ) Extend (m goldmark .Markdown ) {
m .Parser ().AddOptions (
parser .WithParagraphTransformers (
util .Prioritized (NewTableParagraphTransformer (), 200 ),
),
parser .WithASTTransformers (
util .Prioritized (defaultTableASTTransformer , 0 ),
),
)
m .Renderer ().AddOptions (renderer .WithNodeRenderers (
util .Prioritized (NewTableHTMLRenderer (e .options ...), 500 ),
))
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .