Source file src/cmd/vendor/rsc.io/markdown/list.go

     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  
     5  package markdown
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"strings"
    11  )
    12  
    13  type List struct {
    14  	Position
    15  	Bullet rune
    16  	Start  int
    17  	Loose  bool
    18  	Items  []Block // always *Item
    19  }
    20  
    21  type Item struct {
    22  	Position
    23  	Blocks []Block
    24  	width  int
    25  }
    26  
    27  func (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  
    47  func (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  
    62  func (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  
    75  func (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  
    93  type listBuilder struct {
    94  	bullet rune
    95  	num    int
    96  	loose  bool
    97  	item   *itemBuilder
    98  	todo   func() line
    99  }
   100  
   101  func (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
   107  Loose:
   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  
   147  func (b *itemBuilder) build(p buildState) Block {
   148  	b.list.item = nil
   149  	return &Item{p.pos(), p.blocks(), b.width}
   150  }
   151  
   152  func (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  
   160  func (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  
   175  func 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  
   187  func (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
   198  Switch:
   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  
   278  func (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  
   313  func 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  
   320  type Task struct {
   321  	Checked bool
   322  }
   323  
   324  func (x *Task) Inline() {
   325  }
   326  
   327  func (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  
   335  func (x *Task) printMarkdown(buf *bytes.Buffer) {
   336  	x.PrintText(buf)
   337  }
   338  
   339  func (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  
   350  func 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  

View as plain text