class MarkdownIt::RulesBlock::List

Public Class Methods

list(state, startLine, endLine, silent) click to toggle source
# File lib/motion-markdown-it/rules_block/list.rb, line 105
def self.list(state, startLine, endLine, silent)
  isTerminatingParagraph = false
  tight = true

  # if it's indented more than 3 spaces, it should be a code block
  return false if (state.sCount[startLine] - state.blkIndent >= 4)

  # limit conditions when list can interrupt
  # a paragraph (validation mode only)
  if silent && state.parentType == 'paragraph'
    # Next list item should still terminate previous list item;
    #
    # This code can fail if plugins use blkIndent as well as lists,
    # but I hope the spec gets fixed long before that happens.
    #
    if state.tShift[startLine] >= state.blkIndent
      isTerminatingParagraph = true
    end
  end

  # Detect list type and position after marker
  if ((posAfterMarker = skipOrderedListMarker(state, startLine)) >= 0)
    isOrdered   = true
    start       = state.bMarks[startLine] + state.tShift[startLine]
    markerValue = state.src[start, posAfterMarker - start - 1].to_i

    # If we're starting a new ordered list right after
    # a paragraph, it should start with 1.
    return false if isTerminatingParagraph && markerValue != 1
  elsif ((posAfterMarker = skipBulletListMarker(state, startLine)) >= 0)
    isOrdered = false
  else
    return false
  end

  # If we're starting a new unordered list right after
  # a paragraph, first line should not be empty.
  if isTerminatingParagraph
    return false if state.skipSpaces(posAfterMarker) >= state.eMarks[startLine]
  end

  # We should terminate list on style change. Remember first one to compare.
  markerCharCode = charCodeAt(state.src, posAfterMarker - 1)

  # For validation mode we can terminate immediately
  return true if (silent)

  # Start list
  listTokIdx = state.tokens.length

  if (isOrdered)
    start       = state.bMarks[startLine] + state.tShift[startLine]
    markerValue = state.src[start, posAfterMarker - start - 1].to_i
    token       = state.push('ordered_list_open', 'ol', 1)
    if (markerValue != 1)
      token.attrs = [ [ 'start', markerValue ] ]
    end

  else
    token       = state.push('bullet_list_open', 'ul', 1)
  end

  token.map    = listLines = [ startLine, 0 ]
  token.markup = markerCharCode.chr

  #
  # Iterate list items
  #

  nextLine        = startLine
  prevEmptyEnd    = false
  terminatorRules = state.md.block.ruler.getRules('list')

  oldParentType    = state.parentType
  state.parentType = 'list'

  while (nextLine < endLine)
    pos          = posAfterMarker
    max          = state.eMarks[nextLine]

    initial = offset = state.sCount[nextLine] + posAfterMarker - (state.bMarks[startLine] + state.tShift[startLine])

    while pos < max
      ch = charCodeAt(state.src, pos)

      if ch == 0x09
        offset += 4 - (offset + state.bsCount[nextLine]) % 4
      elsif ch == 0x20
        offset += 1
      else
        break
      end

      pos += 1
    end

    contentStart = pos

    if (contentStart >= max)
      # trimming space in "-    \n  3" case, indent is 1 here
      indentAfterMarker = 1
    else
      indentAfterMarker = offset - initial
    end

    # If we have more than 4 spaces, the indent is 1
    # (the rest is just indented code block)
    indentAfterMarker = 1 if (indentAfterMarker > 4)

    # "  -  test"
    #  ^^^^^ - calculating total length of this thing
    indent = initial + indentAfterMarker

    # Run subparser & write tokens
    token        = state.push('list_item_open', 'li', 1)
    token.markup = markerCharCode.chr
    token.map    = itemLines = [ startLine, 0 ]

    oldIndent               = state.blkIndent
    oldTight                = state.tight
    oldTShift               = state.tShift[startLine]
    oldLIndent              = state.sCount[startLine]
    state.blkIndent         = indent
    state.tight             = true
    state.tShift[startLine] = contentStart - state.bMarks[startLine]
    state.sCount[startLine] = offset

    if contentStart >= max && state.isEmpty(startLine + 1)
      # workaround for this case
      # (list item is empty, list terminates before "foo"):
      # ~~~~~~~~
      #   -
      #
      #     foo
      # ~~~~~~~~
      state.line = [state.line + 2, endLine].min
    else
      state.md.block.tokenize(state, startLine, endLine, true)
    end

    # If any of list item is tight, mark list as tight
    if (!state.tight || prevEmptyEnd)
      tight = false
    end
    # Item become loose if finish with empty line,
    # but we should filter last element, because it means list finish
    prevEmptyEnd = (state.line - startLine) > 1 && state.isEmpty(state.line - 1)

    state.blkIndent         = oldIndent
    state.tShift[startLine] = oldTShift
    state.sCount[startLine] = oldLIndent
    state.tight             = oldTight

    token                   = state.push('list_item_close', 'li', -1)
    token.markup            = markerCharCode.chr

    nextLine                = startLine = state.line
    itemLines[1]            = nextLine
    contentStart            = state.bMarks[startLine]

    break if (nextLine >= endLine)

    #
    # Try to check if list is terminated or continued.
    #
    break if (state.sCount[nextLine] < state.blkIndent)

    # fail if terminating block found
    terminate = false
    (0...terminatorRules.length).each do |i|
      if (terminatorRules[i].call(state, nextLine, endLine, true))
        terminate = true
        break
      end
    end
    break if (terminate)

    # fail if list has another type
    if (isOrdered)
      posAfterMarker = skipOrderedListMarker(state, nextLine)
      break if (posAfterMarker < 0)
    else
      posAfterMarker = skipBulletListMarker(state, nextLine)
      break if (posAfterMarker < 0)
    end

    break if (markerCharCode != charCodeAt(state.src, posAfterMarker - 1))
  end

  # Finalize list
  if (isOrdered)
    token = state.push('ordered_list_close', 'ol', -1)
  else
    token = state.push('bullet_list_close', 'ul', -1)
  end
  token.markup = markerCharCode.chr

  listLines[1] = nextLine
  state.line   = nextLine

  state.parentType = oldParentType

  # mark paragraphs tight if needed
  if (tight)
    markTightParagraphs(state, listTokIdx)
  end

  return true
end
markTightParagraphs(state, idx) click to toggle source
# File lib/motion-markdown-it/rules_block/list.rb, line 89
def self.markTightParagraphs(state, idx)
  level = state.level + 2

  i = idx + 2
  l =  state.tokens.length
  while i < l
    if (state.tokens[i].level == level && state.tokens[i].type == 'paragraph_open')
      state.tokens[i + 2].hidden = true
      state.tokens[i].hidden     = true
      i += 2
    end
    i += 1
  end
end
skipBulletListMarker(state, startLine) click to toggle source

Search `[-+*][n ]`, returns next pos after marker on success or -1 on fail.

# File lib/motion-markdown-it/rules_block/list.rb, line 11
def self.skipBulletListMarker(state, startLine)
  pos = state.bMarks[startLine] + state.tShift[startLine]
  max = state.eMarks[startLine]

  marker = charCodeAt(state.src, pos)
  pos   += 1
  # Check bullet
  if (marker != 0x2A && # *
      marker != 0x2D && # -
      marker != 0x2B)   # +
    return -1
  end

  if pos < max
    ch = charCodeAt(state.src, pos)

    if !isSpace(ch)
      # " -test " - is not a list item
      return -1
    end
  end

  return pos
end
skipOrderedListMarker(state, startLine) click to toggle source

Search `d+[n ]`, returns next pos after marker on success or -1 on fail.

# File lib/motion-markdown-it/rules_block/list.rb, line 39
def self.skipOrderedListMarker(state, startLine)
  start = state.bMarks[startLine] + state.tShift[startLine]
  pos   = start
  max   = state.eMarks[startLine]

  # List marker should have at least 2 chars (digit + dot)
  return -1 if (pos + 1 >= max)

  ch   = charCodeAt(state.src, pos)
  pos += 1

  return -1 if ch.nil?
  return -1 if (ch < 0x30 || ch > 0x39) # < 0 || > 9

  while true
    # EOL -> fail
    return -1 if (pos >= max)

    ch   = charCodeAt(state.src, pos)
    pos += 1

    if (ch >= 0x30 && ch <= 0x39) #  >= 0 && <= 9

      # List marker should have no more than 9 digits
      # (prevents integer overflow in browsers)
      return -1 if pos - start >= 10

      next
    end

    # found valid marker
    if (ch === 0x29 || ch === 0x2e) # ')' || '.'
      break
    end

    return -1
  end

  if pos < max
    ch = charCodeAt(state.src, pos)

    if !isSpace(ch)
      # " 1.test " - is not a list item
      return -1
    end
  end
  return pos
end