1// package meta is a extension for the goldmark(http://github.com/yuin/goldmark).
  2//
  3// This extension parses YAML metadata blocks and store metadata to a
  4// parser.Context.
  5package meta
  6
  7import (
  8	"bytes"
  9	"fmt"
 10
 11	"github.com/yuin/goldmark"
 12	gast "github.com/yuin/goldmark/ast"
 13	east "github.com/yuin/goldmark/extension/ast"
 14	"github.com/yuin/goldmark/parser"
 15	"github.com/yuin/goldmark/text"
 16	"github.com/yuin/goldmark/util"
 17
 18	"gopkg.in/yaml.v2"
 19)
 20
 21type data struct {
 22	Map   map[string]interface{}
 23	Items yaml.MapSlice
 24	Error error
 25	Node  gast.Node
 26}
 27
 28var contextKey = parser.NewContextKey()
 29
 30// Option interface sets options for this extension.
 31type Option interface {
 32	metaOption()
 33}
 34
 35// Get returns a YAML metadata.
 36func Get(pc parser.Context) map[string]interface{} {
 37	v := pc.Get(contextKey)
 38	if v == nil {
 39		return nil
 40	}
 41	d := v.(*data)
 42	return d.Map
 43}
 44
 45// TryGet tries to get a YAML metadata.
 46// If there are YAML parsing errors, then nil and error are returned
 47func TryGet(pc parser.Context) (map[string]interface{}, error) {
 48	dtmp := pc.Get(contextKey)
 49	if dtmp == nil {
 50		return nil, nil
 51	}
 52	d := dtmp.(*data)
 53	if d.Error != nil {
 54		return nil, d.Error
 55	}
 56	return d.Map, nil
 57}
 58
 59// GetItems returns a YAML metadata.
 60// GetItems preserves defined key order.
 61func GetItems(pc parser.Context) yaml.MapSlice {
 62	v := pc.Get(contextKey)
 63	if v == nil {
 64		return nil
 65	}
 66	d := v.(*data)
 67	return d.Items
 68}
 69
 70// TryGetItems returns a YAML metadata.
 71// TryGetItems preserves defined key order.
 72// If there are YAML parsing errors, then nil and erro are returned.
 73func TryGetItems(pc parser.Context) (yaml.MapSlice, error) {
 74	dtmp := pc.Get(contextKey)
 75	if dtmp == nil {
 76		return nil, nil
 77	}
 78	d := dtmp.(*data)
 79	if d.Error != nil {
 80		return nil, d.Error
 81	}
 82	return d.Items, nil
 83}
 84
 85type metaParser struct {
 86}
 87
 88var defaultParser = &metaParser{}
 89
 90// NewParser returns a BlockParser that can parse YAML metadata blocks.
 91func NewParser() parser.BlockParser {
 92	return defaultParser
 93}
 94
 95func isSeparator(line []byte) bool {
 96	line = util.TrimRightSpace(util.TrimLeftSpace(line))
 97	for i := 0; i < len(line); i++ {
 98		if line[i] != '-' {
 99			return false
100		}
101	}
102	return true
103}
104
105func (b *metaParser) Trigger() []byte {
106	return []byte{'-'}
107}
108
109func (b *metaParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
110	linenum, _ := reader.Position()
111	if linenum != 0 {
112		return nil, parser.NoChildren
113	}
114	line, _ := reader.PeekLine()
115	if isSeparator(line) {
116		return gast.NewTextBlock(), parser.NoChildren
117	}
118	return nil, parser.NoChildren
119}
120
121func (b *metaParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
122	line, segment := reader.PeekLine()
123	if isSeparator(line) && !util.IsBlank(line) {
124		reader.Advance(segment.Len())
125		return parser.Close
126	}
127	node.Lines().Append(segment)
128	return parser.Continue | parser.NoChildren
129}
130
131func (b *metaParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
132	lines := node.Lines()
133	var buf bytes.Buffer
134	for i := 0; i < lines.Len(); i++ {
135		segment := lines.At(i)
136		buf.Write(segment.Value(reader.Source()))
137	}
138	d := &data{}
139	d.Node = node
140	meta := map[string]interface{}{}
141	if err := yaml.Unmarshal(buf.Bytes(), &meta); err != nil {
142		d.Error = err
143	} else {
144		d.Map = meta
145	}
146
147	metaMapSlice := yaml.MapSlice{}
148	if err := yaml.Unmarshal(buf.Bytes(), &metaMapSlice); err != nil {
149		d.Error = err
150	} else {
151		d.Items = metaMapSlice
152	}
153
154	pc.Set(contextKey, d)
155
156	if d.Error == nil {
157		node.Parent().RemoveChild(node.Parent(), node)
158	}
159}
160
161func (b *metaParser) CanInterruptParagraph() bool {
162	return false
163}
164
165func (b *metaParser) CanAcceptIndentedLine() bool {
166	return false
167}
168
169type astTransformer struct {
170	transformerConfig
171}
172
173type transformerConfig struct {
174	// Renders metadata as an html table.
175	Table bool
176
177	// Stores metadata in ast.Document.Meta().
178	StoresInDocument bool
179}
180
181type transformerOption interface {
182	Option
183
184	// SetMetaOption sets options for the metadata parser.
185	SetMetaOption(*transformerConfig)
186}
187
188var _ transformerOption = &withTable{}
189
190type withTable struct {
191	value bool
192}
193
194func (o *withTable) metaOption() {}
195
196func (o *withTable) SetMetaOption(m *transformerConfig) {
197	m.Table = o.value
198}
199
200// WithTable is a functional option that renders a YAML metadata as a table.
201func WithTable() Option {
202	return &withTable{
203		value: true,
204	}
205}
206
207var _ transformerOption = &withStoresInDocument{}
208
209type withStoresInDocument struct {
210	value bool
211}
212
213func (o *withStoresInDocument) metaOption() {}
214
215func (o *withStoresInDocument) SetMetaOption(c *transformerConfig) {
216	c.StoresInDocument = o.value
217}
218
219// WithStoresInDocument is a functional option that parser will store YAML meta in ast.Document.Meta().
220func WithStoresInDocument() Option {
221	return &withStoresInDocument{
222		value: true,
223	}
224}
225
226func newTransformer(opts ...transformerOption) parser.ASTTransformer {
227	p := &astTransformer{
228		transformerConfig: transformerConfig{
229			Table:            false,
230			StoresInDocument: false,
231		},
232	}
233	for _, o := range opts {
234		o.SetMetaOption(&p.transformerConfig)
235	}
236	return p
237}
238
239func (a *astTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) {
240	dtmp := pc.Get(contextKey)
241	if dtmp == nil {
242		return
243	}
244	d := dtmp.(*data)
245	if d.Error != nil {
246		msg := gast.NewString([]byte(fmt.Sprintf("<!-- %s -->", d.Error)))
247		msg.SetCode(true)
248		d.Node.AppendChild(d.Node, msg)
249		return
250	}
251
252	if a.Table {
253		meta := GetItems(pc)
254		if meta == nil {
255			return
256		}
257		table := east.NewTable()
258		alignments := []east.Alignment{}
259		for range meta {
260			alignments = append(alignments, east.AlignNone)
261		}
262		row := east.NewTableRow(alignments)
263		for _, item := range meta {
264			cell := east.NewTableCell()
265			cell.AppendChild(cell, gast.NewString([]byte(fmt.Sprintf("%v", item.Key))))
266			row.AppendChild(row, cell)
267		}
268		table.AppendChild(table, east.NewTableHeader(row))
269
270		row = east.NewTableRow(alignments)
271		for _, item := range meta {
272			cell := east.NewTableCell()
273			cell.AppendChild(cell, gast.NewString([]byte(fmt.Sprintf("%v", item.Value))))
274			row.AppendChild(row, cell)
275		}
276		table.AppendChild(table, row)
277		node.InsertBefore(node, node.FirstChild(), table)
278	}
279
280	if a.StoresInDocument {
281		for k, v := range d.Map {
282			node.AddMeta(k, v)
283		}
284	}
285}
286
287type meta struct {
288	options []Option
289}
290
291// Meta is a extension for the goldmark.
292var Meta = &meta{}
293
294// New returns a new Meta extension.
295func New(opts ...Option) goldmark.Extender {
296	e := &meta{
297		options: opts,
298	}
299	return e
300}
301
302// Extend implements goldmark.Extender.
303func (e *meta) Extend(m goldmark.Markdown) {
304	topts := []transformerOption{}
305	for _, opt := range e.options {
306		if topt, ok := opt.(transformerOption); ok {
307			topts = append(topts, topt)
308		}
309	}
310	m.Parser().AddOptions(
311		parser.WithBlockParsers(
312			util.Prioritized(NewParser(), 0),
313		),
314	)
315	m.Parser().AddOptions(
316		parser.WithASTTransformers(
317			util.Prioritized(newTransformer(topts...), 0),
318		),
319	)
320}