atx_heading.go (5878B)
1 package parser 2 3 import ( 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. 10 type HeadingConfig struct { 11 AutoHeadingID bool 12 Attribute bool 13 } 14 15 // SetOption implements SetOptioner. 16 func (b *HeadingConfig) SetOption(name OptionName, value interface{}) { 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. 26 type HeadingOption interface { 27 Option 28 SetHeadingOption(*HeadingConfig) 29 } 30 31 // AutoHeadingID is an option name that enables auto IDs for headings. 32 const optAutoHeadingID OptionName = "AutoHeadingID" 33 34 type withAutoHeadingID struct { 35 } 36 37 func (o *withAutoHeadingID) SetParserOption(c *Config) { 38 c.Options[optAutoHeadingID] = true 39 } 40 41 func (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. 47 func WithAutoHeadingID() HeadingOption { 48 return &withAutoHeadingID{} 49 } 50 51 type withHeadingAttribute struct { 52 Option 53 } 54 55 func (o *withHeadingAttribute) SetHeadingOption(p *HeadingConfig) { 56 p.Attribute = true 57 } 58 59 // WithHeadingAttribute is a functional option that enables custom heading attributes. 60 func WithHeadingAttribute() HeadingOption { 61 return &withHeadingAttribute{WithAttribute()} 62 } 63 64 type atxHeadingParser struct { 65 HeadingConfig 66 } 67 68 // NewATXHeadingParser return a new BlockParser that can parse ATX headings. 69 func NewATXHeadingParser(opts ...HeadingOption) BlockParser { 70 p := &atxHeadingParser{} 71 for _, o := range opts { 72 o.SetHeadingOption(&p.HeadingConfig) 73 } 74 return p 75 } 76 77 func (b *atxHeadingParser) Trigger() []byte { 78 return []byte{'#'} 79 } 80 81 func (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 start := i + l 102 if start >= len(line) { 103 start = len(line) - 1 104 } 105 origstart := start 106 stop := len(line) - util.TrimRightSpaceLength(line) 107 108 node := ast.NewHeading(level) 109 parsed := false 110 if b.Attribute { // handles special case like ### heading ### {#id} 111 start-- 112 closureClose := -1 113 closureOpen := -1 114 for j := start; j < stop; { 115 c := line[j] 116 if util.IsEscapedPunctuation(line, j) { 117 j += 2 118 } else if util.IsSpace(c) && j < stop-1 && line[j+1] == '#' { 119 closureOpen = j + 1 120 k := j + 1 121 for ; k < stop && line[k] == '#'; k++ { 122 } 123 closureClose = k 124 break 125 } else { 126 j++ 127 } 128 } 129 if closureClose > 0 { 130 reader.Advance(closureClose) 131 attrs, ok := ParseAttributes(reader) 132 rest, _ := reader.PeekLine() 133 parsed = ok && util.IsBlank(rest) 134 if parsed { 135 for _, attr := range attrs { 136 node.SetAttribute(attr.Name, attr.Value) 137 } 138 node.Lines().Append(text.NewSegment(segment.Start+start+1-segment.Padding, segment.Start+closureOpen-segment.Padding)) 139 } 140 } 141 } 142 if !parsed { 143 start = origstart 144 stop := len(line) - util.TrimRightSpaceLength(line) 145 if stop <= start { // empty headings like '##[space]' 146 stop = start 147 } else { 148 i = stop - 1 149 for ; line[i] == '#' && i >= start; i-- { 150 } 151 if i != stop-1 && !util.IsSpace(line[i]) { 152 i = stop - 1 153 } 154 i++ 155 stop = i 156 } 157 158 if len(util.TrimRight(line[start:stop], []byte{'#'})) != 0 { // empty heading like '### ###' 159 node.Lines().Append(text.NewSegment(segment.Start+start-segment.Padding, segment.Start+stop-segment.Padding)) 160 } 161 } 162 return node, NoChildren 163 } 164 165 func (b *atxHeadingParser) Continue(node ast.Node, reader text.Reader, pc Context) State { 166 return Close 167 } 168 169 func (b *atxHeadingParser) Close(node ast.Node, reader text.Reader, pc Context) { 170 if b.Attribute { 171 _, ok := node.AttributeString("id") 172 if !ok { 173 parseLastLineAttributes(node, reader, pc) 174 } 175 } 176 177 if b.AutoHeadingID { 178 id, ok := node.AttributeString("id") 179 if !ok { 180 generateAutoHeadingID(node.(*ast.Heading), reader, pc) 181 } else { 182 pc.IDs().Put(id.([]byte)) 183 } 184 } 185 } 186 187 func (b *atxHeadingParser) CanInterruptParagraph() bool { 188 return true 189 } 190 191 func (b *atxHeadingParser) CanAcceptIndentedLine() bool { 192 return false 193 } 194 195 func generateAutoHeadingID(node *ast.Heading, reader text.Reader, pc Context) { 196 var line []byte 197 lastIndex := node.Lines().Len() - 1 198 if lastIndex > -1 { 199 lastLine := node.Lines().At(lastIndex) 200 line = lastLine.Value(reader.Source()) 201 } 202 headingID := pc.IDs().Generate(line, ast.KindHeading) 203 node.SetAttribute(attrNameID, headingID) 204 } 205 206 func parseLastLineAttributes(node ast.Node, reader text.Reader, pc Context) { 207 lastIndex := node.Lines().Len() - 1 208 if lastIndex < 0 { // empty headings 209 return 210 } 211 lastLine := node.Lines().At(lastIndex) 212 line := lastLine.Value(reader.Source()) 213 lr := text.NewReader(line) 214 var attrs Attributes 215 var ok bool 216 var start text.Segment 217 var sl int 218 var end text.Segment 219 for { 220 c := lr.Peek() 221 if c == text.EOF { 222 break 223 } 224 if c == '\\' { 225 lr.Advance(1) 226 if lr.Peek() == '{' { 227 lr.Advance(1) 228 } 229 continue 230 } 231 if c == '{' { 232 sl, start = lr.Position() 233 attrs, ok = ParseAttributes(lr) 234 _, end = lr.Position() 235 lr.SetPosition(sl, start) 236 } 237 lr.Advance(1) 238 } 239 if ok && util.IsBlank(line[end.Start:]) { 240 for _, attr := range attrs { 241 node.SetAttribute(attr.Name, attr.Value) 242 } 243 lastLine.Stop = lastLine.Start + start.Start 244 node.Lines().Set(lastIndex, lastLine) 245 } 246 }