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