Handle yaml frontmatter (#11016)
Add goldmark-meta to render yaml frontmatter as a table Fix #5377 Signed-off-by: Andrew Thornton <art27@cantab.net>tokarchuk/v1.17
parent
c02dee8bc4
commit
0be25e2150
@ -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), |
||||
), |
||||
) |
||||
} |
||||
} |
Loading…
Reference in new issue