goldmark-mmd

A different way of handling Markdown Metadata
git clone git://src.gearsix.net/goldmark-mmd
Log | Files | Refs | Atom | README | LICENSE

commit 0fd67d318d6cf0b4f699d3122fac7f47c2cbf5ec
parent 2f5532564c3caca81076e4ba3b73c8d42da12e05
Author: yuin <yuin@inforno.net>
Date:   Thu, 30 May 2019 18:29:33 +0900

Initial commit

Diffstat:
A.gitignore | 13+++++++++++++
AREADME.md | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ago.mod | 8++++++++
Ago.sum | 7+++++++
Ameta.go | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ameta_test.go | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 534 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,13 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test +*.pprof + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out diff --git a/README.md b/README.md @@ -0,0 +1,147 @@ +goldmark-meta +========================= + +goldmark-meta is an extension for the [goldmark](http://github.com/yuin/goldmark) +that allows you to define document metadata in YAML format. + +Usage +-------------------- + +### Installation + +``` +go get github.com/yuin/goldmark-meta +``` + +### Markdown syntax + +YAML metadata block is a leaf block that can not have any markdown element +as a child. + +YAML metadata must start with a **YAML metadata separator**. +This separator must be at first line of the document. + +A **YAML metadata separator** is a line that only `-` is repeated. + +YAML metadata must end with a **YAML metadata separator**. + +You can define objects as a 1st level item. At deeper level, you can define +any kind of YAML element. + +Example: + +``` +--- +Title: goldmark-meta +Summary: Add YAML metadata to the document +Tags: + - markdown + - goldmark +--- + +# Heading 1 +``` + + +### Access the metadata + +```go +import ( + "bytes" + "fmt" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark-meta" +) + +func main() { + markdown := goldmark.New( + goldmark.WithExtensions( + meta.Meta, + ), + ) + source := `--- +Title: goldmark-meta +Summary: Add YAML metadata to the document +Tags: + - markdown + - goldmark +--- + +# Hello goldmark-meta +` + + var buf bytes.Buffer + context := parser.NewContext() + if err := markdown.Convert([]byte(source), &buf, parser.WithContext(context)); err != nil { + panic(err) + } + metaData := meta.Get(context) + title := metaData["Title"] + fmt.Print(title) +} +``` + +### Render the metadata as a table + +You need to add `extension.TableHTMLRenderer` or the `Table` extension to +render metadata as a table. + +```go +import ( + "bytes" + "fmt" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/util" + "github.com/yuin/goldmark-meta" +) + +func main() { + markdown := goldmark.New( + goldmark.WithExtensions( + meta.New(meta.WithTable()), + ), + goldmark.WithRendererOptions( + renderer.WithNodeRenderers( + util.Prioritized(extension.NewTableHTMLRenderer(), 500), + ), + ), + ) + // OR + // markdown := goldmark.New( + // goldmark.WithExtensions( + // meta.New(meta.WithTable()), + // extension.Table, + // ), + // ) + source := `--- +Title: goldmark-meta +Summary: Add YAML metadata to the document +Tags: + - markdown + - goldmark +--- + +# Hello goldmark-meta +` + + var buf bytes.Buffer + if err := markdown.Convert([]byte(source), &buf); err != nil { + panic(err) + } + fmt.Print(buf.String()) +} +``` + + +License +-------------------- +MIT + +Author +-------------------- +Yusuke Inuzuka diff --git a/go.mod b/go.mod @@ -0,0 +1,8 @@ +module github.com/yuin/goldmark-meta + +go 1.12 + +require ( + github.com/yuin/goldmark v1.0.7 + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/go.sum b/go.sum @@ -0,0 +1,7 @@ +github.com/yuin/goldmark v1.0.6 h1:nUVNNVbzZ43ns0NYQCgcGJhGk8Yx4usPESqZoycGyyg= +github.com/yuin/goldmark v1.0.6/go.mod h1:GAOXQunDkMxip+WLt/Bb4n4TEwap/Bit20gguI0UhOE= +github.com/yuin/goldmark v1.0.7 h1:xw+1To1nUhJcMwyAA1O7mHrSpgqKRE12601tiBbH5Po= +github.com/yuin/goldmark v1.0.7/go.mod h1:GAOXQunDkMxip+WLt/Bb4n4TEwap/Bit20gguI0UhOE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/meta.go b/meta.go @@ -0,0 +1,214 @@ +// package meta is a extension for the goldmark(http://github.com/yuin/goldmark). +// +// This extension parses YAML metadata blocks and store metadata to a +// parser.Context. +package meta + +import ( + "bytes" + "fmt" + "github.com/yuin/goldmark" + gast "github.com/yuin/goldmark/ast" + east "github.com/yuin/goldmark/extension/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" + + "gopkg.in/yaml.v2" +) + +type data struct { + Map map[string]interface{} + Items yaml.MapSlice + Error error + Node gast.Node +} + +var contextKey = parser.NewContextKey() + +// Get returns a YAML metadata. +func Get(pc parser.Context) map[string]interface{} { + v := pc.Get(contextKey) + if v == nil { + return nil + } + d := v.(*data) + return d.Map +} + +// GetItems returns a YAML metadata. +// GetItems preserves defined key order. +func GetItems(pc parser.Context) yaml.MapSlice { + v := pc.Get(contextKey) + if v == nil { + return nil + } + d := v.(*data) + return d.Items +} + +type metaParser struct { +} + +var defaultMetaParser = &metaParser{} + +// NewParser returns a BlockParser that can parse YAML metadata blocks. +func NewParser() parser.BlockParser { + return defaultMetaParser +} + +func isSeparator(line []byte) bool { + line = util.TrimRightSpace(util.TrimLeftSpace(line)) + for i := 0; i < len(line); i++ { + if line[i] != '-' { + return false + } + } + return true +} + +func (b *metaParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) { + linenum, _ := reader.Position() + if linenum != 0 { + return nil, parser.NoChildren + } + line, _ := reader.PeekLine() + if isSeparator(line) { + return gast.NewTextBlock(), parser.NoChildren + } + return nil, parser.NoChildren +} + +func (b *metaParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State { + line, segment := reader.PeekLine() + if isSeparator(line) { + reader.Advance(segment.Len()) + return parser.Close + } + node.Lines().Append(segment) + return parser.Continue | parser.NoChildren +} + +func (b *metaParser) Close(node gast.Node, reader text.Reader, pc parser.Context) { + lines := node.Lines() + var buf bytes.Buffer + for i := 0; i < lines.Len(); i++ { + segment := lines.At(i) + buf.Write(segment.Value(reader.Source())) + } + d := &data{} + d.Node = node + meta := map[string]interface{}{} + if err := yaml.Unmarshal(buf.Bytes(), &meta); err != nil { + d.Error = err + } else { + d.Map = meta + } + + metaMapSlice := yaml.MapSlice{} + if err := yaml.Unmarshal(buf.Bytes(), &metaMapSlice); err != nil { + d.Error = err + } else { + d.Items = metaMapSlice + } + + pc.Set(contextKey, d) + + if d.Error == nil { + node.Parent().RemoveChild(node.Parent(), node) + } +} + +func (b *metaParser) CanInterruptParagraph() bool { + return false +} + +func (b *metaParser) CanAcceptIndentedLine() bool { + return false +} + +type astTransformer struct { +} + +var defaultASTTransformer = &astTransformer{} + +func (a *astTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) { + dtmp := pc.Get(contextKey) + if dtmp == nil { + return + } + d := dtmp.(*data) + if d.Error != nil { + msg := gast.NewString([]byte(fmt.Sprintf("<!-- %s -->", d.Error))) + msg.SetCode(true) + d.Node.AppendChild(d.Node, msg) + return + } + + meta := GetItems(pc) + if meta == nil { + return + } + table := east.NewTable() + alignments := []east.Alignment{} + for range meta { + alignments = append(alignments, east.AlignNone) + } + row := east.NewTableRow(alignments) + for _, item := range meta { + cell := east.NewTableCell() + cell.AppendChild(cell, gast.NewString([]byte(fmt.Sprintf("%v", item.Key)))) + row.AppendChild(row, cell) + } + table.AppendChild(table, east.NewTableHeader(row)) + + row = east.NewTableRow(alignments) + for _, item := range meta { + cell := east.NewTableCell() + cell.AppendChild(cell, gast.NewString([]byte(fmt.Sprintf("%v", item.Value)))) + row.AppendChild(row, cell) + } + table.AppendChild(table, row) + node.InsertBefore(node, node.FirstChild(), table) +} + +// Option is a functional option type for this extension. +type Option func(*meta) + +// WithTable is a functional option that renders a YAML metadata as a table. +func WithTable() Option { + return func(m *meta) { + m.Table = true + } +} + +type meta struct { + Table bool +} + +// Meta is a extension for the goldmark. +var Meta = &meta{} + +// New returns a new Meta extension. +func New(opts ...Option) goldmark.Extender { + e := &meta{} + for _, opt := range opts { + opt(e) + } + return e +} + +func (e *meta) Extend(m goldmark.Markdown) { + m.Parser().AddOptions( + parser.WithBlockParsers( + util.Prioritized(NewParser(), 0), + ), + ) + if e.Table { + m.Parser().AddOptions( + parser.WithASTTransformers( + util.Prioritized(defaultASTTransformer, 0), + ), + ) + } +} diff --git a/meta_test.go b/meta_test.go @@ -0,0 +1,145 @@ +package meta + +import ( + "bytes" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/util" + "testing" +) + +func TestMeta(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + Meta, + ), + ) + source := `--- +Title: goldmark-meta +Summary: Add YAML metadata to the document +Tags: + - markdown + - goldmark +--- + +# Hello goldmark-meta +` + + var buf bytes.Buffer + context := parser.NewContext() + if err := markdown.Convert([]byte(source), &buf, parser.WithContext(context)); err != nil { + panic(err) + } + metaData := Get(context) + title := metaData["Title"] + s, ok := title.(string) + if !ok { + t.Error("Title not found in meta data or is not a string") + } + if s != "goldmark-meta" { + t.Errorf("Title must be %s, but got %v", "goldmark-meta", s) + } + if buf.String() != "<h1>Hello goldmark-meta</h1>\n" { + t.Errorf("should render '<h1>Hello goldmark-meta</h1>', but '%s'", buf.String()) + } + tags, ok := metaData["Tags"].([]interface{}) + if !ok { + t.Error("Tags not found in meta data or is not a slice") + } + if len(tags) != 2 { + t.Error("Tags must be a slice that has 2 elements") + } + if tags[0] != "markdown" { + t.Errorf("Tag#1 must be 'markdown', but got %s", tags[0]) + } + if tags[1] != "goldmark" { + t.Errorf("Tag#2 must be 'goldmark', but got %s", tags[1]) + } +} + +func TestMetaTable(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + New(WithTable()), + ), + goldmark.WithRendererOptions( + renderer.WithNodeRenderers( + util.Prioritized(extension.NewTableHTMLRenderer(), 500), + ), + ), + ) + source := `--- +Title: goldmark-meta +Summary: Add YAML metadata to the document +Tags: + - markdown + - goldmark +--- + +# Hello goldmark-meta +` + + var buf bytes.Buffer + if err := markdown.Convert([]byte(source), &buf); err != nil { + panic(err) + } + if buf.String() != `<table> +<thead> +<tr> +<th>Title</th> +<th>Summary</th> +<th>Tags</th> +</tr> +</thead> +<tbody> +<tr> +<td>goldmark-meta</td> +<td>Add YAML metadata to the document</td> +<td>[markdown goldmark]</td> +</tr> +</tbody> +</table> +<h1>Hello goldmark-meta</h1> +` { + t.Error("invalid table output") + } +} + +func TestMetaError(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + New(WithTable()), + ), + ) + source := `--- +Title: goldmark-meta +Summary: Add YAML metadata to the document +Tags: + - : { + } + - markdown + - goldmark +--- + +# Hello goldmark-meta +` + + var buf bytes.Buffer + if err := markdown.Convert([]byte(source), &buf); err != nil { + panic(err) + } + if buf.String() != `Title: goldmark-meta +Summary: Add YAML metadata to the document +Tags: + - : { + } + - markdown + - goldmark +<!-- yaml: line 3: did not find expected key --> +<h1>Hello goldmark-meta</h1> +` { + t.Error("invalid error output") + } +}