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 }