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