goldmark-mmd

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

meta.go (raw) (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 }