attributes.go (3309B)
1 package attributes
2
3 import (
4 "github.com/yuin/goldmark"
5 "github.com/yuin/goldmark/ast"
6 "github.com/yuin/goldmark/parser"
7 "github.com/yuin/goldmark/text"
8 "github.com/yuin/goldmark/util"
9 )
10
11 // This extension is based on/inspired by https://github.com/mdigger/goldmark-attributes
12 // MIT License
13 // Copyright (c) 2019 Dmitry Sedykh
14
15 var (
16 kindAttributesBlock = ast.NewNodeKind("AttributesBlock")
17
18 defaultParser = new(attrParser)
19 defaultTransformer = new(transformer)
20 attributes goldmark.Extender = new(attrExtension)
21 )
22
23 func New() goldmark.Extender {
24 return attributes
25 }
26
27 type attrExtension struct{}
28
29 func (a *attrExtension) Extend(m goldmark.Markdown) {
30 m.Parser().AddOptions(
31 parser.WithBlockParsers(
32 util.Prioritized(defaultParser, 100)),
33 parser.WithASTTransformers(
34 util.Prioritized(defaultTransformer, 100),
35 ),
36 )
37 }
38
39 type attrParser struct{}
40
41 func (a *attrParser) CanAcceptIndentedLine() bool {
42 return false
43 }
44
45 func (a *attrParser) CanInterruptParagraph() bool {
46 return true
47 }
48
49 func (a *attrParser) Close(node ast.Node, reader text.Reader, pc parser.Context) {
50 }
51
52 func (a *attrParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State {
53 return parser.Close
54 }
55
56 func (a *attrParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) {
57 if attrs, ok := parser.ParseAttributes(reader); ok {
58 // add attributes
59 var node = &attributesBlock{
60 BaseBlock: ast.BaseBlock{},
61 }
62 for _, attr := range attrs {
63 node.SetAttribute(attr.Name, attr.Value)
64 }
65 return node, parser.NoChildren
66 }
67 return nil, parser.RequireParagraph
68 }
69
70 func (a *attrParser) Trigger() []byte {
71 return []byte{'{'}
72 }
73
74 type attributesBlock struct {
75 ast.BaseBlock
76 }
77
78 func (a *attributesBlock) Dump(source []byte, level int) {
79 attrs := a.Attributes()
80 list := make(map[string]string, len(attrs))
81 for _, attr := range attrs {
82 var (
83 name = util.BytesToReadOnlyString(attr.Name)
84 value = util.BytesToReadOnlyString(util.EscapeHTML(attr.Value.([]byte)))
85 )
86 list[name] = value
87 }
88 ast.DumpHelper(a, source, level, list, nil)
89 }
90
91 func (a *attributesBlock) Kind() ast.NodeKind {
92 return kindAttributesBlock
93 }
94
95 type transformer struct{}
96
97 func (a *transformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
98 var attributes = make([]ast.Node, 0, 500)
99 ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
100 if entering && node.Kind() == kindAttributesBlock {
101 // Attributes for fenced code blocks are handled in their own extension,
102 // but note that we currently only support code block attributes when
103 // CodeFences=true.
104 if node.PreviousSibling() != nil && node.PreviousSibling().Kind() != ast.KindFencedCodeBlock && !node.HasBlankPreviousLines() {
105 attributes = append(attributes, node)
106 return ast.WalkSkipChildren, nil
107 }
108 }
109
110 return ast.WalkContinue, nil
111 })
112
113 for _, attr := range attributes {
114 if prev := attr.PreviousSibling(); prev != nil &&
115 prev.Type() == ast.TypeBlock {
116 for _, attr := range attr.Attributes() {
117 if _, found := prev.Attribute(attr.Name); !found {
118 prev.SetAttribute(attr.Name, attr.Value)
119 }
120 }
121 }
122 // remove attributes node
123 attr.Parent().RemoveChild(attr.Parent(), attr)
124 }
125 }