Add goldmark-meta to render yaml frontmatter as a table Fix #5377 Signed-off-by: Andrew Thornton <art27@cantab.net>for-closed-social
@ -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 |
@ -0,0 +1,21 @@ | |||
MIT License | |||
Copyright (c) 2019 Yusuke Inuzuka | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is | |||
furnished to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in all | |||
copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
SOFTWARE. |
@ -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 |
@ -0,0 +1,8 @@ | |||
module github.com/yuin/goldmark-meta | |||
go 1.13 | |||
require ( | |||
github.com/yuin/goldmark v1.1.7 | |||
gopkg.in/yaml.v2 v2.2.2 | |||
) |
@ -0,0 +1,6 @@ | |||
github.com/yuin/goldmark v1.1.7 h1:XiwWADvxJeIM1JbXqthrEhDc19hTMui+o+QaY1hGXlk= | |||
github.com/yuin/goldmark v1.1.7/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | |||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | |||
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= |
@ -0,0 +1,218 @@ | |||
// 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) Trigger() []byte { | |||
return []byte{'-'} | |||
} | |||
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), | |||
), | |||
) | |||
} | |||
} |