1// Copyright 2021 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package markdown 6 7import ( 8 "bytes" 9 "fmt" 10 "strings" 11) 12 13type Heading struct { 14 Position 15 Level int 16 Text *Text 17 // The HTML id attribute. The parser populates this field if 18 // [Parser.HeadingIDs] is true and the heading ends with text like "{#id}". 19 ID string 20} 21 22func (b *Heading) PrintHTML(buf *bytes.Buffer) { 23 fmt.Fprintf(buf, "<h%d", b.Level) 24 if b.ID != "" { 25 fmt.Fprintf(buf, ` id="%s"`, htmlQuoteEscaper.Replace(b.ID)) 26 } 27 buf.WriteByte('>') 28 b.Text.PrintHTML(buf) 29 fmt.Fprintf(buf, "</h%d>\n", b.Level) 30} 31 32func (b *Heading) printMarkdown(buf *bytes.Buffer, s mdState) { 33 // TODO: handle setext headings properly. 34 buf.WriteString(s.prefix) 35 for i := 0; i < b.Level; i++ { 36 buf.WriteByte('#') 37 } 38 buf.WriteByte(' ') 39 // The prefix has already been printed for this line of text. 40 s.prefix = "" 41 b.Text.printMarkdown(buf, s) 42 if b.ID != "" { 43 // A heading text is a block, so it ends in a newline. Move the newline 44 // after the ID. 45 buf.Truncate(buf.Len() - 1) 46 fmt.Fprintf(buf, " {#%s}\n", b.ID) 47 } 48} 49 50func newATXHeading(p *parseState, s line) (line, bool) { 51 peek := s 52 var n int 53 if peek.trimHeading(&n) { 54 s := peek.string() 55 s = trimRightSpaceTab(s) 56 // Remove trailing '#'s. 57 if t := strings.TrimRight(s, "#"); t != trimRightSpaceTab(t) || t == "" { 58 s = t 59 } 60 var id string 61 if p.HeadingIDs { 62 // Parse and remove ID attribute. 63 // It must come before trailing '#'s to more closely follow the spec: 64 // The optional closing sequence of #s must be preceded by spaces or tabs 65 // and may be followed by spaces or tabs only. 66 // But Goldmark allows it to come after. 67 id, s = extractID(p, s) 68 69 // Goldmark is strict about the id syntax. 70 for _, c := range id { 71 if c >= 0x80 || !isLetterDigit(byte(c)) { 72 p.corner = true 73 } 74 } 75 } 76 pos := Position{p.lineno, p.lineno} 77 p.doneBlock(&Heading{pos, n, p.newText(pos, s), id}) 78 return line{}, true 79 } 80 return s, false 81} 82 83// extractID removes an ID attribute from s if one is present. 84// It returns the attribute value and the resulting string. 85// The attribute has the form "{#...}", where the "..." can contain 86// any character other than '}'. 87// The attribute must be followed only by whitespace. 88func extractID(p *parseState, s string) (id, s2 string) { 89 i := strings.LastIndexByte(s, '{') 90 if i < 0 { 91 return "", s 92 } 93 if i+1 >= len(s) || s[i+1] != '#' { 94 p.corner = true // goldmark accepts {} 95 return "", s 96 } 97 j := i + strings.IndexByte(s[i:], '}') 98 if j < 0 || trimRightSpaceTab(s[j+1:]) != "" { 99 return "", s 100 } 101 id = strings.TrimSpace(s[i+2 : j]) 102 if id == "" { 103 p.corner = true // goldmark accepts {#} 104 return "", s 105 } 106 return s[i+2 : j], s[:i] 107} 108 109func newSetextHeading(p *parseState, s line) (line, bool) { 110 var n int 111 peek := s 112 if p.nextB() == p.para() && peek.trimSetext(&n) { 113 p.closeBlock() 114 para, ok := p.last().(*Paragraph) 115 if !ok { 116 return s, false 117 } 118 p.deleteLast() 119 p.doneBlock(&Heading{Position{para.StartLine, p.lineno}, n, para.Text, ""}) 120 return line{}, true 121 } 122 return s, false 123} 124 125func (s *line) trimHeading(width *int) bool { 126 t := *s 127 t.trimSpace(0, 3, false) 128 if !t.trim('#') { 129 return false 130 } 131 n := 1 132 for n < 6 && t.trim('#') { 133 n++ 134 } 135 if !t.trimSpace(1, 1, true) { 136 return false 137 } 138 *width = n 139 *s = t 140 return true 141} 142 143func (s *line) trimSetext(n *int) bool { 144 t := *s 145 t.trimSpace(0, 3, false) 146 c := t.peek() 147 if c == '-' || c == '=' { 148 for t.trim(c) { 149 } 150 t.skipSpace() 151 if t.eof() { 152 if c == '=' { 153 *n = 1 154 } else { 155 *n = 2 156 } 157 return true 158 } 159 } 160 return false 161} 162