goldmark-mmd

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

meta.go (6568B)


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