1package parser
2
3import (
4 "github.com/yuin/goldmark/ast"
5 "github.com/yuin/goldmark/text"
6 "github.com/yuin/goldmark/util"
7)
8
9// A HeadingConfig struct is a data structure that holds configuration of the renderers related to headings.
10type HeadingConfig struct {
11 AutoHeadingID bool
12 Attribute bool
13}
14
15// SetOption implements SetOptioner.
16func (b *HeadingConfig) SetOption(name OptionName, _ any) {
17 switch name {
18 case optAutoHeadingID:
19 b.AutoHeadingID = true
20 case optAttribute:
21 b.Attribute = true
22 }
23}
24
25// A HeadingOption interface sets options for heading parsers.
26type HeadingOption interface {
27 Option
28 SetHeadingOption(*HeadingConfig)
29}
30
31// AutoHeadingID is an option name that enables auto IDs for headings.
32const optAutoHeadingID OptionName = "AutoHeadingID"
33
34type withAutoHeadingID struct {
35}
36
37func (o *withAutoHeadingID) SetParserOption(c *Config) {
38 c.Options[optAutoHeadingID] = true
39}
40
41func (o *withAutoHeadingID) SetHeadingOption(p *HeadingConfig) {
42 p.AutoHeadingID = true
43}
44
45// WithAutoHeadingID is a functional option that enables custom heading ids and
46// auto generated heading ids.
47func WithAutoHeadingID() HeadingOption {
48 return &withAutoHeadingID{}
49}
50
51type withHeadingAttribute struct {
52 Option
53}
54
55func (o *withHeadingAttribute) SetHeadingOption(p *HeadingConfig) {
56 p.Attribute = true
57}
58
59// WithHeadingAttribute is a functional option that enables custom heading attributes.
60func WithHeadingAttribute() HeadingOption {
61 return &withHeadingAttribute{WithAttribute()}
62}
63
64type atxHeadingParser struct {
65 HeadingConfig
66}
67
68// NewATXHeadingParser return a new BlockParser that can parse ATX headings.
69func NewATXHeadingParser(opts ...HeadingOption) BlockParser {
70 p := &atxHeadingParser{}
71 for _, o := range opts {
72 o.SetHeadingOption(&p.HeadingConfig)
73 }
74 return p
75}
76
77func (b *atxHeadingParser) Trigger() []byte {
78 return []byte{'#'}
79}
80
81func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
82 line, segment := reader.PeekLine()
83 pos := pc.BlockOffset()
84 if pos < 0 {
85 return nil, NoChildren
86 }
87 i := pos
88 for ; i < len(line) && line[i] == '#'; i++ {
89 }
90 level := i - pos
91 if i == pos || level > 6 {
92 return nil, NoChildren
93 }
94 if i == len(line) { // alone '#' (without a new line character)
95 return ast.NewHeading(level), NoChildren
96 }
97 l := util.TrimLeftSpaceLength(line[i:])
98 if l == 0 {
99 return nil, NoChildren
100 }
101
102 start := min(i+l, len(line)-1)
103 node := ast.NewHeading(level)
104 hl := text.NewSegment(
105 segment.Start+start-segment.Padding,
106 segment.Start+len(line)-segment.Padding)
107 hl = hl.TrimRightSpace(reader.Source())
108 if hl.Len() == 0 {
109 reader.AdvanceToEOL()
110 return node, NoChildren
111 }
112
113 if b.Attribute {
114 node.Lines().Append(hl)
115 parseLastLineAttributes(node, reader, pc)
116 hl = node.Lines().At(0)
117 node.Lines().Clear()
118 }
119
120 // handle closing sequence of '#' characters
121 line = hl.Value(reader.Source())
122 stop := len(line)
123 if stop == 0 { // empty headings like '##[space]'
124 stop = 0
125 } else {
126 i = stop - 1
127 for ; line[i] == '#' && i > 0; i-- {
128 }
129 if i == 0 && line[0] == '#' { // empty headings like '### ###'
130 reader.AdvanceToEOL()
131 return node, NoChildren
132 }
133 if i != stop-1 && util.IsSpace(line[i]) {
134 stop = i
135 stop -= util.TrimRightSpaceLength(line[0:stop])
136 }
137 }
138 hl.Stop = hl.Start + stop
139 node.Lines().Append(hl)
140 reader.AdvanceToEOL()
141
142 return node, NoChildren
143}
144
145func (b *atxHeadingParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
146 return Close
147}
148
149func (b *atxHeadingParser) Close(node ast.Node, reader text.Reader, pc Context) {
150 if b.AutoHeadingID {
151 id, ok := node.AttributeString("id")
152 if !ok {
153 generateAutoHeadingID(node.(*ast.Heading), reader, pc)
154 } else {
155 pc.IDs().Put(id.([]byte))
156 }
157 }
158}
159
160func (b *atxHeadingParser) CanInterruptParagraph() bool {
161 return true
162}
163
164func (b *atxHeadingParser) CanAcceptIndentedLine() bool {
165 return false
166}
167
168func generateAutoHeadingID(node *ast.Heading, reader text.Reader, pc Context) {
169 var line []byte
170 lastIndex := node.Lines().Len() - 1
171 if lastIndex > -1 {
172 lastLine := node.Lines().At(lastIndex)
173 line = lastLine.Value(reader.Source())
174 }
175 headingID := pc.IDs().Generate(line, ast.KindHeading)
176 node.SetAttribute(attrNameID, headingID)
177}
178
179func parseLastLineAttributes(node ast.Node, reader text.Reader, _ Context) {
180 lastIndex := node.Lines().Len() - 1
181 if lastIndex < 0 { // empty headings
182 return
183 }
184 lastLine := node.Lines().At(lastIndex)
185 line := lastLine.Value(reader.Source())
186 lr := text.NewReader(line)
187 var start text.Segment
188 var sl int
189 for {
190 c := lr.Peek()
191 if c == text.EOF || c == '\n' {
192 break
193 }
194 if c == '\\' {
195 lr.Advance(1)
196 if util.IsPunct(lr.Peek()) {
197 lr.Advance(1)
198 }
199 continue
200 }
201 if c == '{' {
202 sl, start = lr.Position()
203 attrs, ok := ParseAttributes(lr)
204 if ok {
205 if nl, _ := lr.PeekLine(); nl == nil || util.IsBlank(nl) {
206 for _, attr := range attrs {
207 node.SetAttribute(attr.Name, attr.Value)
208 }
209 lastLine.Stop = lastLine.Start + start.Start
210 lastLine = lastLine.TrimRightSpace(reader.Source())
211 node.Lines().Set(lastIndex, lastLine)
212 return
213 }
214 }
215 lr.SetPosition(sl, start)
216 }
217 lr.Advance(1)
218 }
219}