// Copyright 2021 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package markdown import ( "bytes" "fmt" "strings" ) type List struct { Position Bullet rune Start int Loose bool Items []Block // always *Item } type Item struct { Position Blocks []Block width int } func (b *List) PrintHTML(buf *bytes.Buffer) { if b.Bullet == '.' || b.Bullet == ')' { buf.WriteString("\n") } else { buf.WriteString("\n") } } func (b *List) printMarkdown(buf *bytes.Buffer, s mdState) { if buf.Len() > 0 && buf.Bytes()[buf.Len()-1] != '\n' { buf.WriteByte('\n') } s.bullet = b.Bullet s.num = b.Start for i, item := range b.Items { if i > 0 && b.Loose { buf.WriteByte('\n') } item.printMarkdown(buf, s) s.num++ } } func (b *Item) printMarkdown(buf *bytes.Buffer, s mdState) { var marker string if s.bullet == '.' || s.bullet == ')' { marker = fmt.Sprintf("%d%c ", s.num, s.bullet) } else { marker = fmt.Sprintf("%c ", s.bullet) } marker = strings.Repeat(" ", b.width-len(marker)) + marker s.prefix1 = s.prefix + marker s.prefix += strings.Repeat(" ", len(marker)) printMarkdownBlocks(b.Blocks, buf, s) } func (b *Item) PrintHTML(buf *bytes.Buffer) { buf.WriteString("
  • ") if len(b.Blocks) > 0 { if _, ok := b.Blocks[0].(*Text); !ok { buf.WriteString("\n") } } for i, c := range b.Blocks { c.PrintHTML(buf) if i+1 < len(b.Blocks) { if _, ok := c.(*Text); ok { buf.WriteString("\n") } } } buf.WriteString("
  • \n") } type listBuilder struct { bullet rune num int loose bool item *itemBuilder todo func() line } func (b *listBuilder) build(p buildState) Block { blocks := p.blocks() pos := p.pos() // list can have wrong pos b/c extend dance. pos.EndLine = blocks[len(blocks)-1].Pos().EndLine Loose: for i, c := range blocks { c := c.(*Item) if i+1 < len(blocks) { if blocks[i+1].Pos().StartLine-c.EndLine > 1 { b.loose = true break Loose } } for j, d := range c.Blocks { endLine := d.Pos().EndLine if j+1 < len(c.Blocks) { if c.Blocks[j+1].Pos().StartLine-endLine > 1 { b.loose = true break Loose } } } } if !b.loose { for _, c := range blocks { c := c.(*Item) for i, d := range c.Blocks { if p, ok := d.(*Paragraph); ok { c.Blocks[i] = p.Text } } } } return &List{ pos, b.bullet, b.num, b.loose, p.blocks(), } } func (b *itemBuilder) build(p buildState) Block { b.list.item = nil return &Item{p.pos(), p.blocks(), b.width} } func (c *listBuilder) extend(p *parseState, s line) (line, bool) { d := c.item if d != nil && s.trimSpace(d.width, d.width, true) || d == nil && s.isBlank() { return s, true } return s, false } func (c *itemBuilder) extend(p *parseState, s line) (line, bool) { if s.isBlank() && !c.haveContent { return s, false } if s.isBlank() { // Goldmark does this and apparently commonmark.js too. // Not sure why it is necessary. return line{}, true } if !s.isBlank() { c.haveContent = true } return s, true } func newListItem(p *parseState, s line) (line, bool) { if list, ok := p.curB().(*listBuilder); ok && list.todo != nil { s = list.todo() list.todo = nil return s, true } if p.startListItem(&s) { return s, true } return s, false } func (p *parseState) startListItem(s *line) bool { t := *s n := 0 for i := 0; i < 3; i++ { if !t.trimSpace(1, 1, false) { break } n++ } bullet := t.peek() var num int Switch: switch bullet { default: return false case '-', '*', '+': t.trim(bullet) n++ case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': for j := t.i; ; j++ { if j >= len(t.text) { return false } c := t.text[j] if c == '.' || c == ')' { // success bullet = c j++ n += j - t.i t.i = j break Switch } if c < '0' || '9' < c { return false } if j-t.i >= 9 { return false } num = num*10 + int(c) - '0' } } if !t.trimSpace(1, 1, true) { return false } n++ tt := t m := 0 for i := 0; i < 3 && tt.trimSpace(1, 1, false); i++ { m++ } if !tt.trimSpace(1, 1, true) { n += m t = tt } // point of no return var list *listBuilder if c, ok := p.nextB().(*listBuilder); ok { list = c } if list == nil || list.bullet != rune(bullet) { // “When the first list item in a list interrupts a paragraph—that is, // when it starts on a line that would otherwise count as // paragraph continuation text—then (a) the lines Ls must // not begin with a blank line, // and (b) if the list item is ordered, the start number must be 1.” if list == nil && p.para() != nil && (t.isBlank() || (bullet == '.' || bullet == ')') && num != 1) { // Goldmark and Dingus both seem to get this wrong // (or the words above don't mean what we think they do). // when the paragraph that could be continued // is inside a block quote. // See testdata/extra.txt 117.md. p.corner = true return false } list = &listBuilder{bullet: rune(bullet), num: num} p.addBlock(list) } b := &itemBuilder{list: list, width: n, haveContent: !t.isBlank()} list.todo = func() line { p.addBlock(b) list.item = b return t } return true } // GitHub task list extension func (p *parseState) taskList(list *List) { for _, item := range list.Items { item := item.(*Item) if len(item.Blocks) == 0 { continue } var text *Text switch b := item.Blocks[0].(type) { default: continue case *Paragraph: text = b.Text case *Text: text = b } if len(text.Inline) < 1 { continue } pl, ok := text.Inline[0].(*Plain) if !ok { continue } s := pl.Text if len(s) < 4 || s[0] != '[' || s[2] != ']' || (s[1] != ' ' && s[1] != 'x' && s[1] != 'X') { continue } if s[3] != ' ' && s[3] != '\t' { p.corner = true // goldmark does not require the space continue } text.Inline = append([]Inline{&Task{Checked: s[1] == 'x' || s[1] == 'X'}, &Plain{Text: s[len("[x]"):]}}, text.Inline[1:]...) } } func ins(first Inline, x []Inline) []Inline { x = append(x, nil) copy(x[1:], x) x[0] = first return x } type Task struct { Checked bool } func (x *Task) Inline() { } func (x *Task) PrintHTML(buf *bytes.Buffer) { buf.WriteString("`) } func (x *Task) printMarkdown(buf *bytes.Buffer) { x.PrintText(buf) } func (x *Task) PrintText(buf *bytes.Buffer) { buf.WriteByte('[') if x.Checked { buf.WriteByte('x') } else { buf.WriteByte(' ') } buf.WriteByte(']') buf.WriteByte(' ') } func listCorner(list *List) bool { for _, item := range list.Items { item := item.(*Item) if len(item.Blocks) == 0 { // Goldmark mishandles what follows; see testdata/extra.txt 111.md. return true } switch item.Blocks[0].(type) { case *List, *ThematicBreak, *CodeBlock: // Goldmark mishandles a list with various block items inside it. return true } } return false }