# File lib/asciidoctor/lexer.rb, line 288
  def self.next_block(reader, parent, attributes = {}, options = {})
    # Skip ahead to the block content
    skipped = reader.skip_blank_lines

    # bail if we've reached the end of the parent block or document
    return nil unless reader.has_more_lines?

    text_only = options[:text]
    # check for option to find list item text only
    # if skipped a line, assume a list continuation was
    # used and block content is acceptable
    if text_only && skipped > 0
      options.delete(:text)
      text_only = false
    end
    
    parse_metadata = options.fetch(:parse_metadata, true)
    #parse_sections = options.fetch(:parse_sections, false)

    document = parent.document
    parent_context = parent.is_a?(Block) ? parent.context : nil
    block = nil
    style = nil
    explicit_style = nil

    while reader.has_more_lines? && block.nil?
      # if parsing metadata, read until there is no more to read
      if parse_metadata && parse_block_metadata_line(reader, document, attributes, options)
        reader.advance
        next
      #elsif parse_sections && parent_context.nil? && is_next_line_section?(reader, attributes)
      #  block, attributes = next_section(reader, parent, attributes)
      #  break
      end

      # QUESTION introduce parsing context object?
      this_line = reader.get_line
      delimited_block = false
      block_context = nil
      terminator = nil
      # QUESTION put this inside call to rekey attributes?
      if attributes[1]
        style, explicit_style = parse_style_attribute(attributes)
      end

      if delimited_blk_match = is_delimited_block?(this_line, true)
        delimited_block = true
        block_context = delimited_blk_match.context
        terminator = delimited_blk_match.terminator
        if !style
          style = attributes['style'] = block_context.to_s
        elsif style != block_context.to_s
          if delimited_blk_match.masq.include? style
            block_context = style.to_sym
          elsif delimited_blk_match.masq.include?('admonition') && ADMONITION_STYLES.include?(style)
            block_context = :admonition
          else
            puts "asciidoctor: WARNING: line #{reader.lineno}: invalid style for #{block_context} block: #{style}"
            style = block_context.to_s
          end
        end
      end

      if !delimited_block

        # this loop only executes once; used for flow control
        # break once a block is found or at end of loop
        # returns nil if the line must be dropped
        # Implementation note - while(true) is twice as fast as loop
        while true

          # process lines verbatim
          if !style.nil? && COMPLIANCE[:strict_verbatim_paragraphs] && VERBATIM_STYLES.include?(style)
            block_context = style.to_sym
            reader.unshift_line this_line
            # advance to block parsing =>
            break
          end

          # process lines normally
          if !text_only
            # NOTE we're letting break lines (ruler, page_break, etc) have attributes
            if (match = this_line.match(REGEXP[:break_line]))
              block = Block.new(parent, BREAK_LINES[match[0][0..2]])
              break

            # TODO make this a media_blk and handle image, video & audio
            elsif (match = this_line.match(REGEXP[:media_blk_macro]))
              blk_ctx = match[1].to_sym
              block = Block.new(parent, blk_ctx)
              if blk_ctx == :image
                posattrs = ['alt', 'width', 'height']
              elsif blk_ctx == :video
                posattrs = ['poster', 'width', 'height']
              else
                posattrs = []
              end

              unless style.nil? || explicit_style
                attributes['alt'] = style if blk_ctx == :image
                attributes.delete('style')
                style = nil
              end

              block.parse_attributes(match[3], posattrs,
                  :unescape_input => (blk_ctx == :image),
                  :sub_input => true,
                  :sub_result => false,
                  :into => attributes)
              target = block.sub_attributes(match[2])
              if target.empty?
                # drop the line if target resolves to nothing
                return nil
              end

              attributes['target'] = target
              block.title = attributes.delete('title') if attributes.has_key?('title')
              if blk_ctx == :image
                document.register(:images, target)
                attributes['alt'] ||= File.basename(target, File.extname(target))
                # QUESTION should video or audio have an auto-numbered caption?
                block.assign_caption attributes.delete('caption'), 'figure'
              end
              break

            # NOTE we're letting the toc macro have attributes
            elsif (match = this_line.match(REGEXP[:toc]))
              block = Block.new(parent, :toc)
              block.parse_attributes(match[1], [], :sub_result => false, :into => attributes)
              break

            end
          end

          # haven't found anything yet, continue
          if (match = this_line.match(REGEXP[:colist]))
            block = Block.new(parent, :colist)
            attributes['style'] = 'arabic'
            items = []
            block.buffer = items
            reader.unshift_line this_line
            expected_index = 1
            begin
              # might want to move this check to a validate method
              if match[1].to_i != expected_index
                puts "asciidoctor: WARNING: line #{reader.lineno + 1}: callout list item index: expected #{expected_index} got #{match[1]}"
              end
              list_item = next_list_item(reader, block, match)
              expected_index += 1
              if !list_item.nil?
                items << list_item
                coids = document.callouts.callout_ids(items.size)
                if !coids.empty?
                  list_item.attributes['coids'] = coids
                else
                  puts "asciidoctor: WARNING: line #{reader.lineno}: no callouts refer to list item #{items.size}"
                end
              end
            end while reader.has_more_lines? && match = reader.peek_line.match(REGEXP[:colist])

            document.callouts.next_list
            break

          elsif (match = this_line.match(REGEXP[:ulist]))
            reader.unshift_line this_line
            block = next_outline_list(reader, :ulist, parent)
            break

          elsif (match = this_line.match(REGEXP[:olist]))
            reader.unshift_line this_line
            block = next_outline_list(reader, :olist, parent)
            # QUESTION move this logic to next_outline_list?
            if !(attributes.has_key? 'style') && !(block.attributes.has_key? 'style')
              marker = block.buffer.first.marker
              if marker.start_with? '.'
                # first one makes more sense, but second on is AsciiDoc-compliant
                #attributes['style'] = (ORDERED_LIST_STYLES[block.level - 1] || ORDERED_LIST_STYLES.first).to_s
                attributes['style'] = (ORDERED_LIST_STYLES[marker.length - 1] || ORDERED_LIST_STYLES.first).to_s
              else
                style = ORDERED_LIST_STYLES.detect{|s| marker.match(ORDERED_LIST_MARKER_PATTERNS[s]) }
                attributes['style'] = (style || ORDERED_LIST_STYLES.first).to_s
              end
            end
            break

          elsif (match = this_line.match(REGEXP[:dlist]))
            reader.unshift_line this_line
            block = next_labeled_list(reader, match, parent)
            break

          elsif (style == 'float' || style == 'discrete') && is_section_title?(this_line, reader.peek_line)
            reader.unshift_line this_line
            float_id, float_title, float_level, _ = parse_section_title(reader, document)
            float_id ||= attributes['id'] if attributes.has_key?('id')
            block = Block.new(parent, :floating_title)
            if float_id.nil? || float_id.empty?
              # FIXME remove hack of creating throwaway Section to get at the generate_id method
              tmp_sect = Section.new(parent)
              tmp_sect.title = float_title
              block.id = tmp_sect.generate_id
            else
              block.id = float_id
            end
            document.register(:ids, [block.id, float_title]) if block.id
            block.level = float_level
            block.title = float_title
            break

          # FIXME create another set for "passthrough" styles
          elsif !style.nil? && style != 'normal'
            if PARAGRAPH_STYLES.include?(style)
              block_context = style.to_sym
              reader.unshift_line this_line
              # advance to block parsing =>
              break
            elsif ADMONITION_STYLES.include?(style)
              block_context = :admonition
              reader.unshift_line this_line
              # advance to block parsing =>
              break
            else
              puts "asciidoctor: WARNING: line #{reader.lineno}: invalid style for paragraph: #{style}"
              style = nil
              # continue to process paragraph
            end
          end

          break_at_list = (skipped == 0 && parent_context.to_s.end_with?('list'))

          # a literal paragraph is contiguous lines starting at least one space
          if style != 'normal' && this_line.match(REGEXP[:lit_par])
            # So we need to actually include this one in the grab_lines group
            reader.unshift_line this_line
            buffer = reader.grab_lines_until(
                :break_on_blank_lines => true,
                :break_on_list_continuation => true,
                :preserve_last_line => true) {|line|
              # a preceding blank line (skipped > 0) indicates we are in a list continuation
              # and therefore we should not break at a list item
              # (this won't stop breaking on item of same level since we've already parsed them out)
              # QUESTION can we turn this block into a lambda or function call?
              (break_at_list && line.match(REGEXP[:any_list])) ||
              (COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line])))
            }

            reset_block_indent! buffer

            block = Block.new(parent, :literal, buffer)
            # a literal gets special meaning inside of a definition list
            if LIST_CONTEXTS.include?(parent_context)
              attributes['options'] ||= []
              # TODO this feels hacky, better way to distinguish from explicit literal block?
              attributes['options'] << 'listparagraph'
            end

          # a paragraph is contiguous nonblank/noncontinuation lines
          else
            reader.unshift_line this_line
            buffer = reader.grab_lines_until(
                :break_on_blank_lines => true,
                :break_on_list_continuation => true,
                :preserve_last_line => true,
                :skip_line_comments => true) {|line|
              # a preceding blank line (skipped > 0) indicates we are in a list continuation
              # and therefore we should not break at a list item
              # (this won't stop breaking on item of same level since we've already parsed them out)
              # QUESTION can we turn this block into a lambda or function call?
              (break_at_list && line.match(REGEXP[:any_list])) ||
              (COMPLIANCE[:block_terminates_paragraph] && (is_delimited_block?(line) || line.match(REGEXP[:attr_line])))
            }

            # NOTE we need this logic because we've asked the reader to skip
            # line comments, which may leave us w/ an empty buffer if those
            # were the only lines found
            if buffer.empty?
              # call get_line since the reader preserved the last line
              reader.get_line
              return nil
            end

            catalog_inline_anchors(buffer.join, document)

            first_line = buffer.first
            if !text_only && (admonition_match = first_line.match(REGEXP[:admonition_inline]))
              buffer[0] = admonition_match.post_match.lstrip
              block = Block.new(parent, :admonition, buffer)
              attributes['style'] = admonition_match[1]
              attributes['name'] = admonition_name = admonition_match[1].downcase
              attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
            elsif !text_only && COMPLIANCE[:markdown_syntax] && first_line.start_with?('> ')
              buffer.map! {|line|
                if line.start_with?('> ')
                  line[2..-1]
                elsif line.chomp == '>'
                  line[1..-1]
                else
                  line
                end
              }

              if buffer.last.start_with?('-- ')
                attribution, citetitle = buffer.pop[3..-1].split(', ')
                buffer.pop while buffer.last.chomp.empty?
                buffer[-1] = buffer.last.chomp
              else
                attribution, citetitle = nil
              end
              attributes['style'] = 'quote'
              attributes['attribution'] = attribution unless attribution.nil?
              attributes['citetitle'] = citetitle unless citetitle.nil?
              # NOTE will only detect headings that are floating titles (not section titles)
              # TODO could assume a floating title when inside a block context
              block = build_block(:quote, :complex, false, parent, Reader.new(buffer), attributes)
            elsif !text_only && buffer.size > 1 && first_line.start_with?('"') &&
                buffer.last.start_with?('-- ') && buffer[-2].chomp.end_with?('"')
              buffer[0] = first_line[1..-1]
              attribution, citetitle = buffer.pop[3..-1].split(', ')
              buffer.pop while buffer.last.chomp.empty?
              buffer[-1] = buffer.last.chomp.chop
              attributes['style'] = 'quote'
              attributes['attribution'] = attribution unless attribution.nil?
              attributes['citetitle'] = citetitle unless citetitle.nil?
              block = Block.new(parent, :quote, buffer)
              #block = Block.new(parent, :quote)
              #block << Block.new(block, :paragraph, buffer)
            else
              # QUESTION is this necessary?
              #if style == 'normal' && [' ', "\t"].include?(buffer.first[0..0])
              #  # QUESTION should we only trim leading blanks?
              #  buffer.map! &:lstrip
              #end

              block = Block.new(parent, :paragraph, buffer)
            end
          end

          # forbid loop from executing more than once
          break
        end
      end

      # either delimited block or styled paragraph
      if block.nil? && !block_context.nil?
        # abstract and partintro should be handled by open block
        # FIXME kind of hackish...need to sort out how to generalize this
        block_context = :open if block_context == :abstract || block_context == :partintro

        case block_context
        when :admonition
          attributes['name'] = admonition_name = style.downcase
          attributes['caption'] ||= document.attributes["#{admonition_name}-caption"]
          block = build_block(block_context, :complex, terminator, parent, reader, attributes)

        when :comment
          reader.grab_lines_until(:break_on_blank_lines => true, :chomp_last_line => false)
          return nil

        when :example
          block = build_block(block_context, :complex, terminator, parent, reader, attributes, {:supports_caption => true})

        when :listing, :fenced_code, :source
          if block_context == :fenced_code
            style = attributes['style'] = 'source'
            lang = this_line[3..-1].strip
            attributes['language'] = lang unless lang.empty?
            terminator = terminator[0..2] if terminator.length > 3
          elsif block_context == :source
            AttributeList.rekey(attributes, [nil, 'language', 'linenums'])
          end
          block = build_block(:listing, :verbatim, terminator, parent, reader, attributes, {:supports_caption => true})

        when :literal
          block = build_block(block_context, :verbatim, terminator, parent, reader, attributes)
        
        when :pass
          block = build_block(block_context, :simple, terminator, parent, reader, attributes)

        when :open, :sidebar
          block = build_block(block_context, :complex, terminator, parent, reader, attributes)

        when :table
          block_reader = Reader.new reader.grab_lines_until(:terminator => terminator, :skip_line_comments => true)
          case terminator[0..0]
            when ','
              attributes['format'] = 'csv'
            when ':'
              attributes['format'] = 'dsv'
          end
          block = next_table(block_reader, parent, attributes)

        when :quote, :verse
          AttributeList.rekey(attributes, [nil, 'attribution', 'citetitle'])
          block = build_block(block_context, (block_context == :verse ? :verbatim : :complex), terminator, parent, reader, attributes)

        else
          # this should only happen if there is a misconfiguration
          raise "Unsupported block type #{block_context} at line #{reader.lineno}"
        end
      end
    end

    # when looking for nested content, one or more line comments, comment
    # blocks or trailing attribute lists could leave us without a block,
    # so handle accordingly
    # REVIEW we may no longer need this check
    if !block.nil?
      # REVIEW seems like there is a better way to organize this wrap-up
      block.id      ||= attributes['id'] if attributes.has_key?('id')
      block.title     = attributes['title'] unless block.title?
      block.caption ||= attributes.delete('caption')
      # AsciiDoc always use [id] as the reftext in HTML output,
      # but I'd like to do better in Asciidoctor
      if block.id && block.title? && !attributes.has_key?('reftext')
        document.register(:ids, [block.id, block.title])
      end
      block.update_attributes(attributes)

      if block.context == :listing || block.context == :literal
        catalog_callouts(block.buffer.join, document)
      end
    end

    block
  end