• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 List struct {
14	Position
15	Bullet rune
16	Start  int
17	Loose  bool
18	Items  []Block // always *Item
19}
20
21type Item struct {
22	Position
23	Blocks []Block
24	width  int
25}
26
27func (b *List) PrintHTML(buf *bytes.Buffer) {
28	if b.Bullet == '.' || b.Bullet == ')' {
29		buf.WriteString("<ol")
30		if b.Start != 1 {
31			fmt.Fprintf(buf, " start=\"%d\"", b.Start)
32		}
33		buf.WriteString(">\n")
34	} else {
35		buf.WriteString("<ul>\n")
36	}
37	for _, c := range b.Items {
38		c.PrintHTML(buf)
39	}
40	if b.Bullet == '.' || b.Bullet == ')' {
41		buf.WriteString("</ol>\n")
42	} else {
43		buf.WriteString("</ul>\n")
44	}
45}
46
47func (b *List) printMarkdown(buf *bytes.Buffer, s mdState) {
48	if buf.Len() > 0 && buf.Bytes()[buf.Len()-1] != '\n' {
49		buf.WriteByte('\n')
50	}
51	s.bullet = b.Bullet
52	s.num = b.Start
53	for i, item := range b.Items {
54		if i > 0 && b.Loose {
55			buf.WriteByte('\n')
56		}
57		item.printMarkdown(buf, s)
58		s.num++
59	}
60}
61
62func (b *Item) printMarkdown(buf *bytes.Buffer, s mdState) {
63	var marker string
64	if s.bullet == '.' || s.bullet == ')' {
65		marker = fmt.Sprintf("%d%c ", s.num, s.bullet)
66	} else {
67		marker = fmt.Sprintf("%c ", s.bullet)
68	}
69	marker = strings.Repeat(" ", b.width-len(marker)) + marker
70	s.prefix1 = s.prefix + marker
71	s.prefix += strings.Repeat(" ", len(marker))
72	printMarkdownBlocks(b.Blocks, buf, s)
73}
74
75func (b *Item) PrintHTML(buf *bytes.Buffer) {
76	buf.WriteString("<li>")
77	if len(b.Blocks) > 0 {
78		if _, ok := b.Blocks[0].(*Text); !ok {
79			buf.WriteString("\n")
80		}
81	}
82	for i, c := range b.Blocks {
83		c.PrintHTML(buf)
84		if i+1 < len(b.Blocks) {
85			if _, ok := c.(*Text); ok {
86				buf.WriteString("\n")
87			}
88		}
89	}
90	buf.WriteString("</li>\n")
91}
92
93type listBuilder struct {
94	bullet rune
95	num    int
96	loose  bool
97	item   *itemBuilder
98	todo   func() line
99}
100
101func (b *listBuilder) build(p buildState) Block {
102	blocks := p.blocks()
103	pos := p.pos()
104
105	// list can have wrong pos b/c extend dance.
106	pos.EndLine = blocks[len(blocks)-1].Pos().EndLine
107Loose:
108	for i, c := range blocks {
109		c := c.(*Item)
110		if i+1 < len(blocks) {
111			if blocks[i+1].Pos().StartLine-c.EndLine > 1 {
112				b.loose = true
113				break Loose
114			}
115		}
116		for j, d := range c.Blocks {
117			endLine := d.Pos().EndLine
118			if j+1 < len(c.Blocks) {
119				if c.Blocks[j+1].Pos().StartLine-endLine > 1 {
120					b.loose = true
121					break Loose
122				}
123			}
124		}
125	}
126
127	if !b.loose {
128		for _, c := range blocks {
129			c := c.(*Item)
130			for i, d := range c.Blocks {
131				if p, ok := d.(*Paragraph); ok {
132					c.Blocks[i] = p.Text
133				}
134			}
135		}
136	}
137
138	return &List{
139		pos,
140		b.bullet,
141		b.num,
142		b.loose,
143		p.blocks(),
144	}
145}
146
147func (b *itemBuilder) build(p buildState) Block {
148	b.list.item = nil
149	return &Item{p.pos(), p.blocks(), b.width}
150}
151
152func (c *listBuilder) extend(p *parseState, s line) (line, bool) {
153	d := c.item
154	if d != nil && s.trimSpace(d.width, d.width, true) || d == nil && s.isBlank() {
155		return s, true
156	}
157	return s, false
158}
159
160func (c *itemBuilder) extend(p *parseState, s line) (line, bool) {
161	if s.isBlank() && !c.haveContent {
162		return s, false
163	}
164	if s.isBlank() {
165		// Goldmark does this and apparently commonmark.js too.
166		// Not sure why it is necessary.
167		return line{}, true
168	}
169	if !s.isBlank() {
170		c.haveContent = true
171	}
172	return s, true
173}
174
175func newListItem(p *parseState, s line) (line, bool) {
176	if list, ok := p.curB().(*listBuilder); ok && list.todo != nil {
177		s = list.todo()
178		list.todo = nil
179		return s, true
180	}
181	if p.startListItem(&s) {
182		return s, true
183	}
184	return s, false
185}
186
187func (p *parseState) startListItem(s *line) bool {
188	t := *s
189	n := 0
190	for i := 0; i < 3; i++ {
191		if !t.trimSpace(1, 1, false) {
192			break
193		}
194		n++
195	}
196	bullet := t.peek()
197	var num int
198Switch:
199	switch bullet {
200	default:
201		return false
202	case '-', '*', '+':
203		t.trim(bullet)
204		n++
205	case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
206		for j := t.i; ; j++ {
207			if j >= len(t.text) {
208				return false
209			}
210			c := t.text[j]
211			if c == '.' || c == ')' {
212				// success
213				bullet = c
214				j++
215				n += j - t.i
216				t.i = j
217				break Switch
218			}
219			if c < '0' || '9' < c {
220				return false
221			}
222			if j-t.i >= 9 {
223				return false
224			}
225			num = num*10 + int(c) - '0'
226		}
227
228	}
229	if !t.trimSpace(1, 1, true) {
230		return false
231	}
232	n++
233	tt := t
234	m := 0
235	for i := 0; i < 3 && tt.trimSpace(1, 1, false); i++ {
236		m++
237	}
238	if !tt.trimSpace(1, 1, true) {
239		n += m
240		t = tt
241	}
242
243	// point of no return
244
245	var list *listBuilder
246	if c, ok := p.nextB().(*listBuilder); ok {
247		list = c
248	}
249	if list == nil || list.bullet != rune(bullet) {
250		// “When the first list item in a list interrupts a paragraph—that is,
251		// when it starts on a line that would otherwise count as
252		// paragraph continuation text—then (a) the lines Ls must
253		// not begin with a blank line,
254		// and (b) if the list item is ordered, the start number must be 1.”
255		if list == nil && p.para() != nil && (t.isBlank() || (bullet == '.' || bullet == ')') && num != 1) {
256			// Goldmark and Dingus both seem to get this wrong
257			// (or the words above don't mean what we think they do).
258			// when the paragraph that could be continued
259			// is inside a block quote.
260			// See testdata/extra.txt 117.md.
261			p.corner = true
262			return false
263		}
264		list = &listBuilder{bullet: rune(bullet), num: num}
265		p.addBlock(list)
266	}
267	b := &itemBuilder{list: list, width: n, haveContent: !t.isBlank()}
268	list.todo = func() line {
269		p.addBlock(b)
270		list.item = b
271		return t
272	}
273	return true
274}
275
276// GitHub task list extension
277
278func (p *parseState) taskList(list *List) {
279	for _, item := range list.Items {
280		item := item.(*Item)
281		if len(item.Blocks) == 0 {
282			continue
283		}
284		var text *Text
285		switch b := item.Blocks[0].(type) {
286		default:
287			continue
288		case *Paragraph:
289			text = b.Text
290		case *Text:
291			text = b
292		}
293		if len(text.Inline) < 1 {
294			continue
295		}
296		pl, ok := text.Inline[0].(*Plain)
297		if !ok {
298			continue
299		}
300		s := pl.Text
301		if len(s) < 4 || s[0] != '[' || s[2] != ']' || (s[1] != ' ' && s[1] != 'x' && s[1] != 'X') {
302			continue
303		}
304		if s[3] != ' ' && s[3] != '\t' {
305			p.corner = true // goldmark does not require the space
306			continue
307		}
308		text.Inline = append([]Inline{&Task{Checked: s[1] == 'x' || s[1] == 'X'},
309			&Plain{Text: s[len("[x]"):]}}, text.Inline[1:]...)
310	}
311}
312
313func ins(first Inline, x []Inline) []Inline {
314	x = append(x, nil)
315	copy(x[1:], x)
316	x[0] = first
317	return x
318}
319
320type Task struct {
321	Checked bool
322}
323
324func (x *Task) Inline() {
325}
326
327func (x *Task) PrintHTML(buf *bytes.Buffer) {
328	buf.WriteString("<input ")
329	if x.Checked {
330		buf.WriteString(`checked="" `)
331	}
332	buf.WriteString(`disabled="" type="checkbox">`)
333}
334
335func (x *Task) printMarkdown(buf *bytes.Buffer) {
336	x.PrintText(buf)
337}
338
339func (x *Task) PrintText(buf *bytes.Buffer) {
340	buf.WriteByte('[')
341	if x.Checked {
342		buf.WriteByte('x')
343	} else {
344		buf.WriteByte(' ')
345	}
346	buf.WriteByte(']')
347	buf.WriteByte(' ')
348}
349
350func listCorner(list *List) bool {
351	for _, item := range list.Items {
352		item := item.(*Item)
353		if len(item.Blocks) == 0 {
354			// Goldmark mishandles what follows; see testdata/extra.txt 111.md.
355			return true
356		}
357		switch item.Blocks[0].(type) {
358		case *List, *ThematicBreak, *CodeBlock:
359			// Goldmark mishandles a list with various block items inside it.
360			return true
361		}
362	}
363	return false
364}
365