249 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			249 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Go
		
	
	
	
| // 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
 | |
| }
 | |
| 
 | |
| // TryGet tries to get a YAML metadata.
 | |
| // If there are YAML parsing errors, then nil and error are returned
 | |
| func TryGet(pc parser.Context) (map[string]interface{}, error) {
 | |
| 	dtmp := pc.Get(contextKey)
 | |
| 	if dtmp == nil {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 	d := dtmp.(*data)
 | |
| 	if d.Error != nil {
 | |
| 		return nil, d.Error
 | |
| 	}
 | |
| 	return d.Map, nil
 | |
| }
 | |
| 
 | |
| // 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
 | |
| }
 | |
| 
 | |
| // TryGetItems returns a YAML metadata.
 | |
| // TryGetItems preserves defined key order.
 | |
| // If there are YAML parsing errors, then nil and erro are returned.
 | |
| func TryGetItems(pc parser.Context) (yaml.MapSlice, error) {
 | |
| 	dtmp := pc.Get(contextKey)
 | |
| 	if dtmp == nil {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 	d := dtmp.(*data)
 | |
| 	if d.Error != nil {
 | |
| 		return nil, d.Error
 | |
| 	}
 | |
| 	return d.Items, nil
 | |
| }
 | |
| 
 | |
| 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) && !util.IsBlank(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),
 | |
| 			),
 | |
| 		)
 | |
| 	}
 | |
| }
 |