github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/markup/common/footnote.go (about) 1 // Copyright 2023 The GitBundle Inc. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // Use of this source code is governed by a MIT-style 4 // license that can be found in the LICENSE file. 5 6 // Copyright 2019 Yusuke Inuzuka 7 8 // Most of what follows is a subtly changed version of github.com/yuin/goldmark/extension/footnote.go 9 10 package common 11 12 import ( 13 "bytes" 14 "fmt" 15 "strconv" 16 "unicode" 17 18 "github.com/yuin/goldmark" 19 "github.com/yuin/goldmark/ast" 20 "github.com/yuin/goldmark/parser" 21 "github.com/yuin/goldmark/renderer" 22 "github.com/yuin/goldmark/renderer/html" 23 "github.com/yuin/goldmark/text" 24 "github.com/yuin/goldmark/util" 25 ) 26 27 // CleanValue will clean a value to make it safe to be an id 28 // This function is quite different from the original goldmark function 29 // and more closely matches the output from the shurcooL sanitizer 30 // In particular Unicode letters and numbers are a lot more than a-zA-Z0-9... 31 func CleanValue(value []byte) []byte { 32 value = bytes.TrimSpace(value) 33 rs := bytes.Runes(value) 34 result := make([]rune, 0, len(rs)) 35 needsDash := false 36 for _, r := range rs { 37 switch { 38 case unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_': 39 if needsDash && len(result) > 0 { 40 result = append(result, '-') 41 } 42 needsDash = false 43 result = append(result, unicode.ToLower(r)) 44 default: 45 needsDash = true 46 } 47 } 48 return []byte(string(result)) 49 } 50 51 // Most of what follows is a subtly changed version of github.com/yuin/goldmark/extension/footnote.go 52 53 // A FootnoteLink struct represents a link to a footnote of Markdown 54 // (PHP Markdown Extra) text. 55 type FootnoteLink struct { 56 ast.BaseInline 57 Index int 58 Name []byte 59 } 60 61 // Dump implements Node.Dump. 62 func (n *FootnoteLink) Dump(source []byte, level int) { 63 m := map[string]string{} 64 m["Index"] = fmt.Sprintf("%v", n.Index) 65 m["Name"] = fmt.Sprintf("%v", n.Name) 66 ast.DumpHelper(n, source, level, m, nil) 67 } 68 69 // KindFootnoteLink is a NodeKind of the FootnoteLink node. 70 var KindFootnoteLink = ast.NewNodeKind("GiteaFootnoteLink") 71 72 // Kind implements Node.Kind. 73 func (n *FootnoteLink) Kind() ast.NodeKind { 74 return KindFootnoteLink 75 } 76 77 // NewFootnoteLink returns a new FootnoteLink node. 78 func NewFootnoteLink(index int, name []byte) *FootnoteLink { 79 return &FootnoteLink{ 80 Index: index, 81 Name: name, 82 } 83 } 84 85 // A FootnoteBackLink struct represents a link to a footnote of Markdown 86 // (PHP Markdown Extra) text. 87 type FootnoteBackLink struct { 88 ast.BaseInline 89 Index int 90 Name []byte 91 } 92 93 // Dump implements Node.Dump. 94 func (n *FootnoteBackLink) Dump(source []byte, level int) { 95 m := map[string]string{} 96 m["Index"] = fmt.Sprintf("%v", n.Index) 97 m["Name"] = fmt.Sprintf("%v", n.Name) 98 ast.DumpHelper(n, source, level, m, nil) 99 } 100 101 // KindFootnoteBackLink is a NodeKind of the FootnoteBackLink node. 102 var KindFootnoteBackLink = ast.NewNodeKind("GiteaFootnoteBackLink") 103 104 // Kind implements Node.Kind. 105 func (n *FootnoteBackLink) Kind() ast.NodeKind { 106 return KindFootnoteBackLink 107 } 108 109 // NewFootnoteBackLink returns a new FootnoteBackLink node. 110 func NewFootnoteBackLink(index int, name []byte) *FootnoteBackLink { 111 return &FootnoteBackLink{ 112 Index: index, 113 Name: name, 114 } 115 } 116 117 // A Footnote struct represents a footnote of Markdown 118 // (PHP Markdown Extra) text. 119 type Footnote struct { 120 ast.BaseBlock 121 Ref []byte 122 Index int 123 Name []byte 124 } 125 126 // Dump implements Node.Dump. 127 func (n *Footnote) Dump(source []byte, level int) { 128 m := map[string]string{} 129 m["Index"] = strconv.Itoa(n.Index) 130 m["Ref"] = string(n.Ref) 131 m["Name"] = string(n.Name) 132 ast.DumpHelper(n, source, level, m, nil) 133 } 134 135 // KindFootnote is a NodeKind of the Footnote node. 136 var KindFootnote = ast.NewNodeKind("GiteaFootnote") 137 138 // Kind implements Node.Kind. 139 func (n *Footnote) Kind() ast.NodeKind { 140 return KindFootnote 141 } 142 143 // NewFootnote returns a new Footnote node. 144 func NewFootnote(ref []byte) *Footnote { 145 return &Footnote{ 146 Ref: ref, 147 Index: -1, 148 Name: ref, 149 } 150 } 151 152 // A FootnoteList struct represents footnotes of Markdown 153 // (PHP Markdown Extra) text. 154 type FootnoteList struct { 155 ast.BaseBlock 156 Count int 157 } 158 159 // Dump implements Node.Dump. 160 func (n *FootnoteList) Dump(source []byte, level int) { 161 m := map[string]string{} 162 m["Count"] = fmt.Sprintf("%v", n.Count) 163 ast.DumpHelper(n, source, level, m, nil) 164 } 165 166 // KindFootnoteList is a NodeKind of the FootnoteList node. 167 var KindFootnoteList = ast.NewNodeKind("GiteaFootnoteList") 168 169 // Kind implements Node.Kind. 170 func (n *FootnoteList) Kind() ast.NodeKind { 171 return KindFootnoteList 172 } 173 174 // NewFootnoteList returns a new FootnoteList node. 175 func NewFootnoteList() *FootnoteList { 176 return &FootnoteList{ 177 Count: 0, 178 } 179 } 180 181 var footnoteListKey = parser.NewContextKey() 182 183 type footnoteBlockParser struct{} 184 185 var defaultFootnoteBlockParser = &footnoteBlockParser{} 186 187 // NewFootnoteBlockParser returns a new parser.BlockParser that can parse 188 // footnotes of the Markdown(PHP Markdown Extra) text. 189 func NewFootnoteBlockParser() parser.BlockParser { 190 return defaultFootnoteBlockParser 191 } 192 193 func (b *footnoteBlockParser) Trigger() []byte { 194 return []byte{'['} 195 } 196 197 func (b *footnoteBlockParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { 198 line, segment := reader.PeekLine() 199 pos := pc.BlockOffset() 200 if pos < 0 || line[pos] != '[' { 201 return nil, parser.NoChildren 202 } 203 pos++ 204 if pos > len(line)-1 || line[pos] != '^' { 205 return nil, parser.NoChildren 206 } 207 open := pos + 1 208 closes := 0 209 closure := util.FindClosure(line[pos+1:], '[', ']', false, false) //nolint 210 closes = pos + 1 + closure 211 next := closes + 1 212 if closure > -1 { 213 if next >= len(line) || line[next] != ':' { 214 return nil, parser.NoChildren 215 } 216 } else { 217 return nil, parser.NoChildren 218 } 219 padding := segment.Padding 220 label := reader.Value(text.NewSegment(segment.Start+open-padding, segment.Start+closes-padding)) 221 if util.IsBlank(label) { 222 return nil, parser.NoChildren 223 } 224 item := NewFootnote(label) 225 226 pos = next + 1 - padding 227 if pos >= len(line) { 228 reader.Advance(pos) 229 return item, parser.NoChildren 230 } 231 reader.AdvanceAndSetPadding(pos, padding) 232 return item, parser.HasChildren 233 } 234 235 func (b *footnoteBlockParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { 236 line, _ := reader.PeekLine() 237 if util.IsBlank(line) { 238 return parser.Continue | parser.HasChildren 239 } 240 childpos, padding := util.IndentPosition(line, reader.LineOffset(), 4) 241 if childpos < 0 { 242 return parser.Close 243 } 244 reader.AdvanceAndSetPadding(childpos, padding) 245 return parser.Continue | parser.HasChildren 246 } 247 248 func (b *footnoteBlockParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { 249 var list *FootnoteList 250 if tlist := pc.Get(footnoteListKey); tlist != nil { 251 list = tlist.(*FootnoteList) 252 } else { 253 list = NewFootnoteList() 254 pc.Set(footnoteListKey, list) 255 node.Parent().InsertBefore(node.Parent(), node, list) 256 } 257 node.Parent().RemoveChild(node.Parent(), node) 258 list.AppendChild(list, node) 259 } 260 261 func (b *footnoteBlockParser) CanInterruptParagraph() bool { 262 return true 263 } 264 265 func (b *footnoteBlockParser) CanAcceptIndentedLine() bool { 266 return false 267 } 268 269 type footnoteParser struct{} 270 271 var defaultFootnoteParser = &footnoteParser{} 272 273 // NewFootnoteParser returns a new parser.InlineParser that can parse 274 // footnote links of the Markdown(PHP Markdown Extra) text. 275 func NewFootnoteParser() parser.InlineParser { 276 return defaultFootnoteParser 277 } 278 279 func (s *footnoteParser) Trigger() []byte { 280 // footnote syntax probably conflict with the image syntax. 281 // So we need trigger this parser with '!'. 282 return []byte{'!', '['} 283 } 284 285 func (s *footnoteParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { 286 line, segment := block.PeekLine() 287 pos := 1 288 if len(line) > 0 && line[0] == '!' { 289 pos++ 290 } 291 if pos >= len(line) || line[pos] != '^' { 292 return nil 293 } 294 pos++ 295 if pos >= len(line) { 296 return nil 297 } 298 open := pos 299 closure := util.FindClosure(line[pos:], '[', ']', false, false) //nolint 300 if closure < 0 { 301 return nil 302 } 303 closes := pos + closure 304 value := block.Value(text.NewSegment(segment.Start+open, segment.Start+closes)) 305 block.Advance(closes + 1) 306 307 var list *FootnoteList 308 if tlist := pc.Get(footnoteListKey); tlist != nil { 309 list = tlist.(*FootnoteList) 310 } 311 if list == nil { 312 return nil 313 } 314 index := 0 315 name := []byte{} 316 for def := list.FirstChild(); def != nil; def = def.NextSibling() { 317 d := def.(*Footnote) 318 if bytes.Equal(d.Ref, value) { 319 if d.Index < 0 { 320 list.Count++ 321 d.Index = list.Count 322 val := CleanValue(d.Name) 323 if len(val) == 0 { 324 val = []byte(strconv.Itoa(d.Index)) 325 } 326 d.Name = pc.IDs().Generate(val, KindFootnote) 327 } 328 index = d.Index 329 name = d.Name 330 break 331 } 332 } 333 if index == 0 { 334 return nil 335 } 336 337 return NewFootnoteLink(index, name) 338 } 339 340 type footnoteASTTransformer struct{} 341 342 var defaultFootnoteASTTransformer = &footnoteASTTransformer{} 343 344 // NewFootnoteASTTransformer returns a new parser.ASTTransformer that 345 // insert a footnote list to the last of the document. 346 func NewFootnoteASTTransformer() parser.ASTTransformer { 347 return defaultFootnoteASTTransformer 348 } 349 350 func (a *footnoteASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { 351 var list *FootnoteList 352 if tlist := pc.Get(footnoteListKey); tlist != nil { 353 list = tlist.(*FootnoteList) 354 } else { 355 return 356 } 357 pc.Set(footnoteListKey, nil) 358 for footnote := list.FirstChild(); footnote != nil; { 359 container := footnote 360 next := footnote.NextSibling() 361 if fc := container.LastChild(); fc != nil && ast.IsParagraph(fc) { 362 container = fc 363 } 364 footnoteNode := footnote.(*Footnote) 365 index := footnoteNode.Index 366 name := footnoteNode.Name 367 if index < 0 { 368 list.RemoveChild(list, footnote) 369 } else { 370 container.AppendChild(container, NewFootnoteBackLink(index, name)) 371 } 372 footnote = next 373 } 374 list.SortChildren(func(n1, n2 ast.Node) int { 375 if n1.(*Footnote).Index < n2.(*Footnote).Index { 376 return -1 377 } 378 return 1 379 }) 380 if list.Count <= 0 { 381 list.Parent().RemoveChild(list.Parent(), list) 382 return 383 } 384 385 node.AppendChild(node, list) 386 } 387 388 // FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that 389 // renders FootnoteLink nodes. 390 type FootnoteHTMLRenderer struct { 391 html.Config 392 } 393 394 // NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer. 395 func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { 396 r := &FootnoteHTMLRenderer{ 397 Config: html.NewConfig(), 398 } 399 for _, opt := range opts { 400 opt.SetHTMLOption(&r.Config) 401 } 402 return r 403 } 404 405 // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. 406 func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 407 reg.Register(KindFootnoteLink, r.renderFootnoteLink) 408 reg.Register(KindFootnoteBackLink, r.renderFootnoteBackLink) 409 reg.Register(KindFootnote, r.renderFootnote) 410 reg.Register(KindFootnoteList, r.renderFootnoteList) 411 } 412 413 func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 414 if entering { 415 n := node.(*FootnoteLink) 416 is := strconv.Itoa(n.Index) 417 _, _ = w.WriteString(`<sup id="fnref:`) 418 _, _ = w.Write(n.Name) 419 _, _ = w.WriteString(`"><a href="#fn:`) 420 _, _ = w.Write(n.Name) 421 _, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`) 422 _, _ = w.WriteString(is) 423 _, _ = w.WriteString(`</a></sup>`) 424 } 425 return ast.WalkContinue, nil 426 } 427 428 func (r *FootnoteHTMLRenderer) renderFootnoteBackLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 429 if entering { 430 n := node.(*FootnoteBackLink) 431 _, _ = w.WriteString(` <a href="#fnref:`) 432 _, _ = w.Write(n.Name) 433 _, _ = w.WriteString(`" class="footnote-backref" role="doc-backlink">`) 434 _, _ = w.WriteString("↩︎") 435 _, _ = w.WriteString(`</a>`) 436 } 437 return ast.WalkContinue, nil 438 } 439 440 func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 441 n := node.(*Footnote) 442 if entering { 443 _, _ = w.WriteString(`<li id="fn:`) 444 _, _ = w.Write(n.Name) 445 _, _ = w.WriteString(`" role="doc-endnote"`) 446 if node.Attributes() != nil { 447 html.RenderAttributes(w, node, html.ListItemAttributeFilter) 448 } 449 _, _ = w.WriteString(">\n") 450 } else { 451 _, _ = w.WriteString("</li>\n") 452 } 453 return ast.WalkContinue, nil 454 } 455 456 func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { 457 tag := "div" 458 if entering { 459 _, _ = w.WriteString("<") 460 _, _ = w.WriteString(tag) 461 _, _ = w.WriteString(` class="footnotes" role="doc-endnotes"`) 462 if node.Attributes() != nil { 463 html.RenderAttributes(w, node, html.GlobalAttributeFilter) 464 } 465 _ = w.WriteByte('>') 466 if r.Config.XHTML { 467 _, _ = w.WriteString("\n<hr />\n") 468 } else { 469 _, _ = w.WriteString("\n<hr>\n") 470 } 471 _, _ = w.WriteString("<ol>\n") 472 } else { 473 _, _ = w.WriteString("</ol>\n") 474 _, _ = w.WriteString("</") 475 _, _ = w.WriteString(tag) 476 _, _ = w.WriteString(">\n") 477 } 478 return ast.WalkContinue, nil 479 } 480 481 type footnoteExtension struct{} 482 483 // FootnoteExtension represents the GitBundle Footnote 484 var FootnoteExtension = &footnoteExtension{} 485 486 // Extend extends the markdown converter with the GitBundle Footnote parser 487 func (e *footnoteExtension) Extend(m goldmark.Markdown) { 488 m.Parser().AddOptions( 489 parser.WithBlockParsers( 490 util.Prioritized(NewFootnoteBlockParser(), 999), 491 ), 492 parser.WithInlineParsers( 493 util.Prioritized(NewFootnoteParser(), 101), 494 ), 495 parser.WithASTTransformers( 496 util.Prioritized(NewFootnoteASTTransformer(), 999), 497 ), 498 ) 499 m.Renderer().AddOptions(renderer.WithNodeRenderers( 500 util.Prioritized(NewFootnoteHTMLRenderer(), 500), 501 )) 502 }