Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
248 lines
5.4 KiB
248 lines
5.4 KiB
// 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), |
|
), |
|
) |
|
} |
|
}
|
|
|