diff --git a/README.md b/README.md
index 0ccacbb..8cf7c5a 100644
--- a/README.md
+++ b/README.md
@@ -203,6 +203,18 @@ heading {#id .className attrName=attrValue}
============
```
+### Table extension
+The Table extension implements [Table(extension)](https://github.github.com/gfm/#tables-extension-), as
+defined in [GitHub Flavored Markdown Spec](https://github.github.com/gfm/).
+
+Specs are defined for XHTML, so specs use some deprecated attributes for HTML5.
+
+You can override alignment rendering method via options.
+
+| Functional option | Type | Description |
+| ----------------- | ---- | ----------- |
+| `extension.WithTableCellAlignMethod` | `extension.TableCellAlignMethod` | Option indicates how are table cells aligned. |
+
### Typographer extension
The Typographer extension translates plain ASCII punctuation characters into typographic-punctuation HTML entities.
diff --git a/extension/table.go b/extension/table.go
index 91ba331..081c3c8 100644
--- a/extension/table.go
+++ b/extension/table.go
@@ -15,6 +15,104 @@ import (
"github.com/yuin/goldmark/util"
)
+// TableCellAlignMethod indicates how are table cells aligned in HTML format.indicates how are table cells aligned in HTML format.
+type TableCellAlignMethod int
+
+const (
+ // TableCellAlignDefault renders alignments by default method.
+ // With XHTML, alignments are rendered as an align attribute.
+ // With HTML5, alignments are rendered as a style attribute.
+ TableCellAlignDefault TableCellAlignMethod = iota
+
+ // TableCellAlignAttribute renders alignments as an align attribute.
+ TableCellAlignAttribute
+
+ // TableCellAlignStyle renders alignments as a style attribute.
+ TableCellAlignStyle
+
+ // TableCellAlignNone does not care about alignments.
+ // If you using classes or other styles, you can add these attributes
+ // in an ASTTransformer.
+ TableCellAlignNone
+)
+
+// TableConfig struct holds options for the extension.
+type TableConfig struct {
+ html.Config
+
+ // TableCellAlignMethod indicates how are table celss aligned.
+ TableCellAlignMethod TableCellAlignMethod
+}
+
+// TableOption interface is a functional option interface for the extension.
+type TableOption interface {
+ renderer.Option
+ // SetTableOption sets given option to the extension.
+ SetTableOption(*TableConfig)
+}
+
+// NewTableConfig returns a new Config with defaults.
+func NewTableConfig() TableConfig {
+ return TableConfig{
+ Config: html.NewConfig(),
+ TableCellAlignMethod: TableCellAlignDefault,
+ }
+}
+
+// SetOption implements renderer.SetOptioner.
+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)
+ }
+ }
+}
+
+// WithTableHTMLOptions is functional option that wraps goldmark HTMLRenderer options.
+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
+}
+
+// WithTableCellAlignMethod is a functional option that indicates how are table cells aligned in HTML format.
+func WithTableCellAlignMethod(a TableCellAlignMethod) TableOption {
+ return &withTableCellAlignMethod{a}
+}
+
var tableDelimRegexp = regexp.MustCompile(`^[\s\-\|\:]+$`)
var tableDelimLeft = regexp.MustCompile(`^\s*\:\-+\s*$`)
var tableDelimRight = regexp.MustCompile(`^\s*\-+\:\s*$`)
@@ -131,16 +229,16 @@ func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, reader
// TableHTMLRenderer is a renderer.NodeRenderer implementation that
// renders Table nodes.
type TableHTMLRenderer struct {
- html.Config
+ TableConfig
}
// NewTableHTMLRenderer returns a new TableHTMLRenderer.
-func NewTableHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
+func NewTableHTMLRenderer(opts ...TableOption) renderer.NodeRenderer {
r := &TableHTMLRenderer{
- Config: html.NewConfig(),
+ TableConfig: NewTableConfig(),
}
for _, opt := range opts {
- opt.SetHTMLOption(&r.Config)
+ opt.SetTableOption(&r.TableConfig)
}
return r
}
@@ -281,14 +379,33 @@ func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, nod
tag = "th"
}
if entering {
- align := ""
+ fmt.Fprintf(w, "<%s", tag)
if n.Alignment != ast.AlignNone {
- if _, ok := n.AttributeString("align"); !ok { // Skip align render if overridden
- // TODO: "align" is deprecated. style="text-align:%s" instead?
- align = fmt.Sprintf(` align="%s"`, n.Alignment.String())
+ 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 { // Skip align render if overridden
+ 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.Append(util.StringToReadOnlyBytes(style))
+ n.SetAttributeString("style", cob.Bytes())
}
}
- fmt.Fprintf(w, "<%s", tag)
if n.Attributes() != nil {
if tag == "td" {
html.RenderAttributes(w, n, TableTdCellAttributeFilter) //
@@ -296,7 +413,7 @@ func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, nod
html.RenderAttributes(w, n, TableThCellAttributeFilter) // |
}
}
- fmt.Fprintf(w, "%s>", align)
+ _ = w.WriteByte('>')
} else {
fmt.Fprintf(w, "%s>\n", tag)
}
@@ -304,16 +421,26 @@ func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, nod
}
type table struct {
+ options []TableOption
}
// Table is an extension that allow you to use GFM tables .
-var Table = &table{}
+var Table = &table{
+ options: []TableOption{},
+}
+
+// NewTable returns a new extension with given options.
+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),
))
m.Renderer().AddOptions(renderer.WithNodeRenderers(
- util.Prioritized(NewTableHTMLRenderer(), 500),
+ util.Prioritized(NewTableHTMLRenderer(e.options...), 500),
))
}
diff --git a/extension/table_test.go b/extension/table_test.go
index cea3c8e..5ee23e9 100644
--- a/extension/table_test.go
+++ b/extension/table_test.go
@@ -4,14 +4,20 @@ import (
"testing"
"github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/ast"
+ east "github.com/yuin/goldmark/extension/ast"
+ "github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/testutil"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
)
func TestTable(t *testing.T) {
markdown := goldmark.New(
goldmark.WithRendererOptions(
html.WithUnsafe(),
+ html.WithXHTML(),
),
goldmark.WithExtensions(
Table,
@@ -19,3 +25,333 @@ func TestTable(t *testing.T) {
)
testutil.DoTestCaseFile(markdown, "_test/table.txt", t, testutil.ParseCliCaseArg()...)
}
+
+func TestTableWithAlignDefault(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignDefault),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 1,
+ Description: "Cell with TableCellAlignDefault and XHTML should be rendered as an align attribute",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc |
+defghi |
+
+
+
+
+bar |
+baz |
+
+
+ `,
+ },
+ t,
+ )
+
+ markdown = goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignDefault),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 2,
+ Description: "Cell with TableCellAlignDefault and HTML5 should be rendered as a style attribute",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc |
+defghi |
+
+
+
+
+bar |
+baz |
+
+
+ `,
+ },
+ t,
+ )
+}
+
+func TestTableWithAlignAttribute(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignAttribute),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 1,
+ Description: "Cell with TableCellAlignAttribute and XHTML should be rendered as an align attribute",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc |
+defghi |
+
+
+
+
+bar |
+baz |
+
+
+ `,
+ },
+ t,
+ )
+
+ markdown = goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignAttribute),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 2,
+ Description: "Cell with TableCellAlignAttribute and HTML5 should be rendered as an align attribute",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc |
+defghi |
+
+
+
+
+bar |
+baz |
+
+
+ `,
+ },
+ t,
+ )
+}
+
+type tableStyleTransformer struct {
+}
+
+func (a *tableStyleTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
+ cell := node.FirstChild().FirstChild().FirstChild().(*east.TableCell)
+ cell.SetAttributeString("style", []byte("font-size:1em"))
+}
+
+func TestTableWithAlignStyle(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignStyle),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 1,
+ Description: "Cell with TableCellAlignStyle and XHTML should be rendered as a style attribute",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc |
+defghi |
+
+
+
+
+bar |
+baz |
+
+
+ `,
+ },
+ t,
+ )
+
+ markdown = goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignStyle),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 2,
+ Description: "Cell with TableCellAlignStyle and HTML5 should be rendered as a style attribute",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc |
+defghi |
+
+
+
+
+bar |
+baz |
+
+
+ `,
+ },
+ t,
+ )
+
+ markdown = goldmark.New(
+ goldmark.WithParserOptions(
+ parser.WithASTTransformers(
+ util.Prioritized(&tableStyleTransformer{}, 0),
+ ),
+ ),
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignStyle),
+ ),
+ ),
+ )
+
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 3,
+ Description: "Styled cell should not be broken the style by the alignments",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc |
+defghi |
+
+
+
+
+bar |
+baz |
+
+
+ `,
+ },
+ t,
+ )
+}
+
+func TestTableWithAlignNone(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignNone),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 1,
+ Description: "Cell with TableCellAlignStyle and XHTML should not be rendered",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc |
+defghi |
+
+
+
+
+bar |
+baz |
+
+
+ `,
+ },
+ t,
+ )
+}
diff --git a/util/util.go b/util/util.go
index e4ae5e0..fc1438d 100644
--- a/util/util.go
+++ b/util/util.go
@@ -28,6 +28,7 @@ func NewCopyOnWriteBuffer(buffer []byte) CopyOnWriteBuffer {
}
// Write writes given bytes to the buffer.
+// Write allocate new buffer and clears it at the first time.
func (b *CopyOnWriteBuffer) Write(value []byte) {
if !b.copied {
b.buffer = make([]byte, 0, len(b.buffer)+20)
@@ -36,7 +37,20 @@ func (b *CopyOnWriteBuffer) Write(value []byte) {
b.buffer = append(b.buffer, value...)
}
+// Append appends given bytes to the buffer.
+// Append copy buffer at the first time.
+func (b *CopyOnWriteBuffer) Append(value []byte) {
+ if !b.copied {
+ tmp := make([]byte, len(b.buffer), len(b.buffer)+20)
+ copy(tmp, b.buffer)
+ b.buffer = tmp
+ b.copied = true
+ }
+ b.buffer = append(b.buffer, value...)
+}
+
// WriteByte writes the given byte to the buffer.
+// WriteByte allocate new buffer and clears it at the first time.
func (b *CopyOnWriteBuffer) WriteByte(c byte) {
if !b.copied {
b.buffer = make([]byte, 0, len(b.buffer)+20)
@@ -45,6 +59,18 @@ func (b *CopyOnWriteBuffer) WriteByte(c byte) {
b.buffer = append(b.buffer, c)
}
+// AppendByte appends given bytes to the buffer.
+// AppendByte copy buffer at the first time.
+func (b *CopyOnWriteBuffer) AppendByte(c byte) {
+ if !b.copied {
+ tmp := make([]byte, len(b.buffer), len(b.buffer)+20)
+ copy(tmp, b.buffer)
+ b.buffer = tmp
+ b.copied = true
+ }
+ b.buffer = append(b.buffer, c)
+}
+
// Bytes returns bytes of this buffer.
func (b *CopyOnWriteBuffer) Bytes() []byte {
return b.buffer
|