From bd58441cc141c6455d2ed592a4f1d01adee4211a Mon Sep 17 00:00:00 2001 From: yuin Date: Tue, 21 Jul 2020 19:32:52 +0900 Subject: [PATCH] Fixes #78 --- README.md | 12 ++ extension/table.go | 151 ++++++++++++++++-- extension/table_test.go | 336 ++++++++++++++++++++++++++++++++++++++++ util/util.go | 26 ++++ 4 files changed, 513 insertions(+), 12 deletions(-) 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, "\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: ` + + + + + + + + + + + + +
abcdefghi
barbaz
`, + }, + 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: ` + + + + + + + + + + + + +
abcdefghi
barbaz
`, + }, + 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: ` + + + + + + + + + + + + +
abcdefghi
barbaz
`, + }, + 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: ` + + + + + + + + + + + + +
abcdefghi
barbaz
`, + }, + 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: ` + + + + + + + + + + + + +
abcdefghi
barbaz
`, + }, + 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: ` + + + + + + + + + + + + +
abcdefghi
barbaz
`, + }, + 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: ` + + + + + + + + + + + + +
abcdefghi
barbaz
`, + }, + 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: ` + + + + + + + + + + + + +
abcdefghi
barbaz
`, + }, + 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