...

Source file src/github.com/yuin/goldmark/parser/list.go

Documentation: github.com/yuin/goldmark/parser

     1  package parser
     2  
     3  import (
     4  	"strconv"
     5  
     6  	"github.com/yuin/goldmark/ast"
     7  	"github.com/yuin/goldmark/text"
     8  	"github.com/yuin/goldmark/util"
     9  )
    10  
    11  type listItemType int
    12  
    13  const (
    14  	notList listItemType = iota
    15  	bulletList
    16  	orderedList
    17  )
    18  
    19  var skipListParserKey = NewContextKey()
    20  var emptyListItemWithBlankLines = NewContextKey()
    21  var listItemFlagValue interface{} = true
    22  
    23  // Same as
    24  // `^(([ ]*)([\-\*\+]))(\s+.*)?\n?$`.FindSubmatchIndex or
    25  // `^(([ ]*)(\d{1,9}[\.\)]))(\s+.*)?\n?$`.FindSubmatchIndex
    26  func parseListItem(line []byte) ([6]int, listItemType) {
    27  	i := 0
    28  	l := len(line)
    29  	ret := [6]int{}
    30  	for ; i < l && line[i] == ' '; i++ {
    31  		c := line[i]
    32  		if c == '\t' {
    33  			return ret, notList
    34  		}
    35  	}
    36  	if i > 3 {
    37  		return ret, notList
    38  	}
    39  	ret[0] = 0
    40  	ret[1] = i
    41  	ret[2] = i
    42  	var typ listItemType
    43  	if i < l && (line[i] == '-' || line[i] == '*' || line[i] == '+') {
    44  		i++
    45  		ret[3] = i
    46  		typ = bulletList
    47  	} else if i < l {
    48  		for ; i < l && util.IsNumeric(line[i]); i++ {
    49  		}
    50  		ret[3] = i
    51  		if ret[3] == ret[2] || ret[3]-ret[2] > 9 {
    52  			return ret, notList
    53  		}
    54  		if i < l && (line[i] == '.' || line[i] == ')') {
    55  			i++
    56  			ret[3] = i
    57  		} else {
    58  			return ret, notList
    59  		}
    60  		typ = orderedList
    61  	} else {
    62  		return ret, notList
    63  	}
    64  	if i < l && line[i] != '\n' {
    65  		w, _ := util.IndentWidth(line[i:], 0)
    66  		if w == 0 {
    67  			return ret, notList
    68  		}
    69  	}
    70  	if i >= l {
    71  		ret[4] = -1
    72  		ret[5] = -1
    73  		return ret, typ
    74  	}
    75  	ret[4] = i
    76  	ret[5] = len(line)
    77  	if line[ret[5]-1] == '\n' && line[i] != '\n' {
    78  		ret[5]--
    79  	}
    80  	return ret, typ
    81  }
    82  
    83  func matchesListItem(source []byte, strict bool) ([6]int, listItemType) {
    84  	m, typ := parseListItem(source)
    85  	if typ != notList && (!strict || strict && m[1] < 4) {
    86  		return m, typ
    87  	}
    88  	return m, notList
    89  }
    90  
    91  func calcListOffset(source []byte, match [6]int) int {
    92  	offset := 0
    93  	if match[4] < 0 || util.IsBlank(source[match[4]:]) { // list item starts with a blank line
    94  		offset = 1
    95  	} else {
    96  		offset, _ = util.IndentWidth(source[match[4]:], match[4])
    97  		if offset > 4 { // offseted codeblock
    98  			offset = 1
    99  		}
   100  	}
   101  	return offset
   102  }
   103  
   104  func lastOffset(node ast.Node) int {
   105  	lastChild := node.LastChild()
   106  	if lastChild != nil {
   107  		return lastChild.(*ast.ListItem).Offset
   108  	}
   109  	return 0
   110  }
   111  
   112  type listParser struct {
   113  }
   114  
   115  var defaultListParser = &listParser{}
   116  
   117  // NewListParser returns a new BlockParser that
   118  // parses lists.
   119  // This parser must take precedence over the ListItemParser.
   120  func NewListParser() BlockParser {
   121  	return defaultListParser
   122  }
   123  
   124  func (b *listParser) Trigger() []byte {
   125  	return []byte{'-', '+', '*', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
   126  }
   127  
   128  func (b *listParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
   129  	last := pc.LastOpenedBlock().Node
   130  	if _, lok := last.(*ast.List); lok || pc.Get(skipListParserKey) != nil {
   131  		pc.Set(skipListParserKey, nil)
   132  		return nil, NoChildren
   133  	}
   134  	line, _ := reader.PeekLine()
   135  	match, typ := matchesListItem(line, true)
   136  	if typ == notList {
   137  		return nil, NoChildren
   138  	}
   139  	start := -1
   140  	if typ == orderedList {
   141  		number := line[match[2] : match[3]-1]
   142  		start, _ = strconv.Atoi(string(number))
   143  	}
   144  
   145  	if ast.IsParagraph(last) && last.Parent() == parent {
   146  		// we allow only lists starting with 1 to interrupt paragraphs.
   147  		if typ == orderedList && start != 1 {
   148  			return nil, NoChildren
   149  		}
   150  		//an empty list item cannot interrupt a paragraph:
   151  		if match[4] < 0 || util.IsBlank(line[match[4]:match[5]]) {
   152  			return nil, NoChildren
   153  		}
   154  	}
   155  
   156  	marker := line[match[3]-1]
   157  	node := ast.NewList(marker)
   158  	if start > -1 {
   159  		node.Start = start
   160  	}
   161  	pc.Set(emptyListItemWithBlankLines, nil)
   162  	return node, HasChildren
   163  }
   164  
   165  func (b *listParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
   166  	list := node.(*ast.List)
   167  	line, _ := reader.PeekLine()
   168  	if util.IsBlank(line) {
   169  		if node.LastChild().ChildCount() == 0 {
   170  			pc.Set(emptyListItemWithBlankLines, listItemFlagValue)
   171  		}
   172  		return Continue | HasChildren
   173  	}
   174  
   175  	// "offset" means a width that bar indicates.
   176  	//    -  aaaaaaaa
   177  	// |----|
   178  	//
   179  	// If the indent is less than the last offset like
   180  	// - a
   181  	//  - b          <--- current line
   182  	// it maybe a new child of the list.
   183  	//
   184  	// Empty list items can have multiple blanklines
   185  	//
   186  	// -             <--- 1st item is an empty thus "offset" is unknown
   187  	//
   188  	//
   189  	//   -           <--- current line
   190  	//
   191  	// -> 1 list with 2 blank items
   192  	//
   193  	// So if the last item is an empty, it maybe a new child of the list.
   194  	//
   195  	offset := lastOffset(node)
   196  	lastIsEmpty := node.LastChild().ChildCount() == 0
   197  	indent, _ := util.IndentWidth(line, reader.LineOffset())
   198  
   199  	if indent < offset || lastIsEmpty {
   200  		if indent < 4 {
   201  			match, typ := matchesListItem(line, false) // may have a leading spaces more than 3
   202  			if typ != notList && match[1]-offset < 4 {
   203  				marker := line[match[3]-1]
   204  				if !list.CanContinue(marker, typ == orderedList) {
   205  					return Close
   206  				}
   207  				// Thematic Breaks take precedence over lists
   208  				if isThematicBreak(line[match[3]-1:], 0) {
   209  					isHeading := false
   210  					last := pc.LastOpenedBlock().Node
   211  					if ast.IsParagraph(last) {
   212  						c, ok := matchesSetextHeadingBar(line[match[3]-1:])
   213  						if ok && c == '-' {
   214  							isHeading = true
   215  						}
   216  					}
   217  					if !isHeading {
   218  						return Close
   219  					}
   220  				}
   221  				return Continue | HasChildren
   222  			}
   223  		}
   224  		if !lastIsEmpty {
   225  			return Close
   226  		}
   227  	}
   228  
   229  	if lastIsEmpty && indent < offset {
   230  		return Close
   231  	}
   232  
   233  	// Non empty items can not exist next to an empty list item
   234  	// with blank lines. So we need to close the current list
   235  	//
   236  	// -
   237  	//
   238  	//   foo
   239  	//
   240  	// -> 1 list with 1 blank items and 1 paragraph
   241  	if pc.Get(emptyListItemWithBlankLines) != nil {
   242  		return Close
   243  	}
   244  	return Continue | HasChildren
   245  }
   246  
   247  func (b *listParser) Close(node ast.Node, reader text.Reader, pc Context) {
   248  	list := node.(*ast.List)
   249  
   250  	for c := node.FirstChild(); c != nil && list.IsTight; c = c.NextSibling() {
   251  		if c.FirstChild() != nil && c.FirstChild() != c.LastChild() {
   252  			for c1 := c.FirstChild().NextSibling(); c1 != nil; c1 = c1.NextSibling() {
   253  				if bl, ok := c1.(ast.Node); ok && bl.HasBlankPreviousLines() {
   254  					list.IsTight = false
   255  					break
   256  				}
   257  			}
   258  		}
   259  		if c != node.FirstChild() {
   260  			if bl, ok := c.(ast.Node); ok && bl.HasBlankPreviousLines() {
   261  				list.IsTight = false
   262  			}
   263  		}
   264  	}
   265  
   266  	if list.IsTight {
   267  		for child := node.FirstChild(); child != nil; child = child.NextSibling() {
   268  			for gc := child.FirstChild(); gc != nil; {
   269  				paragraph, ok := gc.(*ast.Paragraph)
   270  				gc = gc.NextSibling()
   271  				if ok {
   272  					textBlock := ast.NewTextBlock()
   273  					textBlock.SetLines(paragraph.Lines())
   274  					child.ReplaceChild(child, paragraph, textBlock)
   275  				}
   276  			}
   277  		}
   278  	}
   279  }
   280  
   281  func (b *listParser) CanInterruptParagraph() bool {
   282  	return true
   283  }
   284  
   285  func (b *listParser) CanAcceptIndentedLine() bool {
   286  	return false
   287  }
   288  

View as plain text