# File lib/asciidoctor/parser.rb, line 415
  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 unless reader.has_more_lines?

    # 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 = options[:text]) && 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
    if (extensions = document.extensions)
      block_extensions = extensions.blocks?
      block_macro_extensions = extensions.block_macros?
    else
      block_extensions = block_macro_extensions = false
    end
    #parent_context = parent.is_a?(Block) ? parent.context : nil
    in_list = ListItem === parent
    block = nil
    style = nil
    explicit_style = nil
    sourcemap = document.sourcemap
    source_location = nil

    while !block && reader.has_more_lines?
      # 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 && is_next_line_section?(reader, attributes)
      #  block, attributes = next_section(reader, parent, attributes)
      #  break
      end

      # QUESTION should we introduce a parsing context object?
      source_location = reader.cursor if sourcemap
      this_line = reader.read_line
      delimited_block = false
      block_context = nil
      cloaked_context = nil
      terminator = nil
      # QUESTION put this inside call to rekey attributes?
      if attributes[1]
        style, explicit_style = parse_style_attribute(attributes, reader)
      end

      if (delimited_blk_match = is_delimited_block? this_line, true)
        delimited_block = true
        block_context = cloaked_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
          elsif block_extensions && extensions.registered_for_block?(style, block_context)
            block_context = style.to_sym
          else
            warn %(asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for #{block_context} block: #{style})
            style = block_context.to_s
          end
        end
      end

      unless 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 && 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
          unless text_only
            first_char = Compliance.markdown_syntax ? this_line.lstrip.chr : this_line.chr
            # NOTE we're letting break lines (horizontal rule, page_break, etc) have attributes
            if (LAYOUT_BREAK_LINES.has_key? first_char) && this_line.length >= 3 &&
                (Compliance.markdown_syntax ? LayoutBreakLinePlusRx : LayoutBreakLineRx) =~ this_line
              block = Block.new(parent, LAYOUT_BREAK_LINES[first_char], :content_model => :empty)
              break

            elsif this_line.end_with?(']') && (match = MediaBlockMacroRx.match(this_line))
              blk_ctx = match[1].to_sym
              block = Block.new(parent, blk_ctx, :content_model => :empty)
              if blk_ctx == :image
                posattrs = ['alt', 'width', 'height']
              elsif blk_ctx == :video
                posattrs = ['poster', 'width', 'height']
              else
                posattrs = []
              end

              unless !style || 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], :attribute_missing => 'drop-line')
              if target.empty?
                # retain as unparsed if attribute-missing is skip
                if document.attributes.fetch('attribute-missing', Compliance.attribute_missing) == 'skip'
                  return Block.new(parent, :paragraph, :content_model => :simple, :source => [this_line])
                # otherwise, drop the line
                else
                  attributes.clear
                  return
                end
              end

              attributes['target'] = target
              # now done down below
              #block.title = attributes.delete('title') if attributes.has_key?('title')
              #if blk_ctx == :image
              #  if attributes.has_key? 'scaledwidth'
              #    # append % to scaledwidth if ends in number (no units present)
              #    if (48..57).include?((attributes['scaledwidth'][-1] || 0).ord)
              #      attributes['scaledwidth'] = %(#{attributes['scaledwidth']}%)
              #    end
              #  end
              #  document.register(:images, target)
              #  attributes['alt'] ||= Helpers.basename(target, true).tr('_-', ' ')
              #  # 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 first_char == 't' && (match = TocBlockMacroRx.match(this_line))
              block = Block.new(parent, :toc, :content_model => :empty)
              block.parse_attributes(match[1], [], :sub_result => false, :into => attributes)
              break

            elsif block_macro_extensions && (match = GenericBlockMacroRx.match(this_line)) &&
                (extension = extensions.registered_for_block_macro?(match[1]))
              target = match[2]
              raw_attributes = match[3]
              if extension.config[:content_model] == :attributes
                unless raw_attributes.empty?
                  document.parse_attributes(raw_attributes, (extension.config[:pos_attrs] || []),
                      :sub_input => true, :sub_result => false, :into => attributes)
                end
              else
                attributes['text'] = raw_attributes
              end
              if (default_attrs = extension.config[:default_attrs])
                default_attrs.each {|k, v| attributes[k] ||= v }
              end
              if (block = extension.process_method[parent, target, attributes.dup])
                attributes.replace block.attributes
              else
                attributes.clear
                return
              end
              break
            end
          end

          # haven't found anything yet, continue
          if (match = CalloutListRx.match(this_line))
            block = List.new(parent, :colist)
            attributes['style'] = 'arabic'
            reader.unshift_line this_line
            expected_index = 1
            # NOTE skip the match on the first time through as we've already done it (emulates begin...while)
            while match || (reader.has_more_lines? && (match = CalloutListRx.match(reader.peek_line)))
              # might want to move this check to a validate method
              if match[1].to_i != expected_index
                # FIXME this lineno - 2 hack means we need a proper look-behind cursor
                warn %(asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: 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
                block << list_item
                coids = document.callouts.callout_ids(block.items.size)
                if !coids.empty?
                  list_item.attributes['coids'] = coids
                else
                  # FIXME this lineno - 2 hack means we need a proper look-behind cursor
                  warn %(asciidoctor: WARNING: #{reader.path}: line #{reader.lineno - 2}: no callouts refer to list item #{block.items.size})
                end
              end
              match = nil
            end

            document.callouts.next_list
            break

          elsif UnorderedListRx =~ this_line
            reader.unshift_line this_line
            block = next_outline_list(reader, :ulist, parent)
            break

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

          elsif (match = DefinitionListRx.match(this_line))
            reader.unshift_line this_line
            block = next_labeled_list(reader, match, parent)
            break

          elsif (style == 'float' || style == 'discrete') &&
              is_section_title?(this_line, (Compliance.underline_style_section_titles ? reader.peek_line(true) : nil))
            reader.unshift_line this_line
            float_id, float_reftext, float_title, float_level, _ = parse_section_title(reader, document)
            attributes['reftext'] = float_reftext if float_reftext
            float_id ||= attributes['id'] if attributes.has_key?('id')
            block = Block.new(parent, :floating_title, :content_model => :empty)
            if float_id.nil_or_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
            block.level = float_level
            block.title = float_title
            break

          # FIXME create another set for "passthrough" styles
          # FIXME make this more DRY!
          elsif style && style != 'normal'
            if PARAGRAPH_STYLES.include?(style)
              block_context = style.to_sym
              cloaked_context = :paragraph
              reader.unshift_line this_line
              # advance to block parsing =>
              break
            elsif ADMONITION_STYLES.include?(style)
              block_context = :admonition
              cloaked_context = :paragraph
              reader.unshift_line this_line
              # advance to block parsing =>
              break
            elsif block_extensions && extensions.registered_for_block?(style, :paragraph)
              block_context = style.to_sym
              cloaked_context = :paragraph
              reader.unshift_line this_line
              # advance to block parsing =>
              break
            else
              warn %(asciidoctor: WARNING: #{reader.prev_line_info}: invalid style for paragraph: #{style})
              style = nil
              # continue to process paragraph
            end
          end

          break_at_list = (skipped == 0 && in_list)

          # a literal paragraph is contiguous lines starting at least one space
          if style != 'normal' && LiteralParagraphRx =~ this_line
            # So we need to actually include this one in the read_lines group
            reader.unshift_line this_line
            lines = read_paragraph_lines reader, break_at_list, :skip_line_comments => text_only

            adjust_indentation! lines

            block = Block.new(parent, :literal, :content_model => :verbatim, :source => lines, :attributes => attributes)
            # a literal gets special meaning inside of a definition list
            # TODO this feels hacky, better way to distinguish from explicit literal block?
            block.set_option('listparagraph') if in_list

          # a paragraph is contiguous nonblank/noncontinuation lines
          else
            reader.unshift_line this_line
            lines = read_paragraph_lines reader, break_at_list, :skip_line_comments => true

            # 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 lines.empty?
              # call advance since the reader preserved the last line
              reader.advance
              return
            end

            catalog_inline_anchors(lines.join(EOL), document)

            first_line = lines[0]
            if !text_only && (admonition_match = AdmonitionParagraphRx.match(first_line))
              lines[0] = admonition_match.post_match.lstrip
              attributes['style'] = admonition_match[1]
              attributes['name'] = admonition_name = admonition_match[1].downcase
              attributes['caption'] ||= document.attributes[%(#{admonition_name}-caption)]
              block = Block.new(parent, :admonition, :content_model => :simple, :source => lines, :attributes => attributes)
            elsif !text_only && Compliance.markdown_syntax && first_line.start_with?('> ')
              lines.map! {|line|
                if line == '>'
                  line[1..-1]
                elsif line.start_with? '> '
                  line[2..-1]
                else
                  line
                end
              }

              if lines[-1].start_with? '-- '
                attribution, citetitle = lines.pop[3..-1].split(', ', 2)
                lines.pop while lines[-1].empty?
              else
                attribution, citetitle = nil
              end
              attributes['style'] = 'quote'
              attributes['attribution'] = attribution if attribution
              attributes['citetitle'] = citetitle if citetitle
              # NOTE will only detect headings that are floating titles (not section titles)
              # TODO could assume a floating title when inside a block context
              # FIXME Reader needs to be created w/ line info
              block = build_block(:quote, :compound, false, parent, Reader.new(lines), attributes)
            elsif !text_only && (blockquote? lines, first_line)
              lines[0] = first_line[1..-1]
              attribution, citetitle = lines.pop[3..-1].split(', ', 2)
              lines.pop while lines[-1].empty?
              # strip trailing quote
              lines[-1] = lines[-1].chop
              attributes['style'] = 'quote'
              attributes['attribution'] = attribution if attribution
              attributes['citetitle'] = citetitle if citetitle
              block = Block.new(parent, :quote, :content_model => :simple, :source => lines, :attributes => attributes)
            else
              # if [normal] is used over an indented paragraph, shift content to left margin
              if style == 'normal'
                # QUESTION do we even need to shift since whitespace is normalized by XML in this case?
                adjust_indentation! lines
              end

              block = Block.new(parent, :paragraph, :content_model => :simple, :source => lines, :attributes => attributes)
            end
          end

          # forbid loop from executing more than once
          break
        end
      end

      # either delimited block or styled paragraph
      if !block && block_context
        # 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, :compound, terminator, parent, reader, attributes)

        when :comment
          build_block(block_context, :skip, terminator, parent, reader, attributes)
          return

        when :example
          block = build_block(block_context, :compound, terminator, parent, reader, attributes)

        when :listing, :fenced_code, :source
          if block_context == :fenced_code
            style = attributes['style'] = 'source'
            language, linenums = this_line[3..-1].tr(' ', '').split(',', 2)
            if !language.nil_or_empty?
              attributes['language'] = language
              attributes['linenums'] = '' unless linenums.nil_or_empty?
            elsif (default_language = document.attributes['source-language'])
              attributes['language'] = default_language
            end
            if !attributes.key?('indent') && document.attributes.key?('source-indent')
              attributes['indent'] = document.attributes['source-indent']
            end
            terminator = terminator[0..2]
          elsif block_context == :source
            AttributeList.rekey(attributes, [nil, 'language', 'linenums'])
            unless attributes.key? 'language'
              if (default_language = document.attributes['source-language'])
                attributes['language'] = default_language
              end
            end
            if !attributes.key?('indent') && document.attributes.key?('source-indent')
              attributes['indent'] = document.attributes['source-indent']
            end
          end
          block = build_block(:listing, :verbatim, terminator, parent, reader, attributes)

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

        when :pass
          block = build_block(block_context, :raw, terminator, parent, reader, attributes)

        when :stem, :latexmath, :asciimath
          if block_context == :stem
            attributes['style'] = if (explicit_stem_syntax = attributes[2])
              explicit_stem_syntax.include?('tex') ? 'latexmath' : 'asciimath'
            elsif (default_stem_syntax = document.attributes['stem']).nil_or_empty?
              'asciimath'
            else
              default_stem_syntax
            end
          end
          block = build_block(:stem, :raw, terminator, parent, reader, attributes)

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

        when :table
          cursor = reader.cursor
          block_reader = Reader.new reader.read_lines_until(:terminator => terminator, :skip_line_comments => true), cursor
          case terminator.chr
            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 : :compound), terminator, parent, reader, attributes)

        else
          if block_extensions && (extension = extensions.registered_for_block?(block_context, cloaked_context))
            # TODO pass cloaked_context to extension somehow (perhaps a new instance for each cloaked_context?)
            if (content_model = extension.config[:content_model]) != :skip
              if !(pos_attrs = extension.config[:pos_attrs] || []).empty?
                AttributeList.rekey(attributes, [nil].concat(pos_attrs))
              end
              if (default_attrs = extension.config[:default_attrs])
                default_attrs.each {|k, v| attributes[k] ||= v }
              end
            end
            block = build_block block_context, content_model, terminator, parent, reader, attributes, :extension => extension
            unless block && content_model != :skip
              attributes.clear
              return
            end
          else
            # this should only happen if there's a misconfiguration
            raise %(Unsupported block type #{block_context} at #{reader.line_info})
          end
        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 nil check
    # FIXME we've got to clean this up, it's horrible!
    if block
      block.source_location = source_location if source_location
      # REVIEW seems like there is a better way to organize this wrap-up
      block.title     = attributes['title'] unless block.title?
      # FIXME HACK don't hardcode logic for alt, caption and scaledwidth on images down here
      if block.context == :image
        resolved_target = attributes['target']
        block.document.register(:images, resolved_target)
        attributes['alt'] ||= Helpers.basename(resolved_target, true).tr('_-', ' ')
        attributes['alt'] = block.sub_specialchars attributes['alt']
        block.assign_caption attributes.delete('caption'), 'figure'
        if (scaledwidth = attributes['scaledwidth'])
          # append % to scaledwidth if ends in number (no units present)
          if (48..57).include?((scaledwidth[-1] || 0).ord)
            attributes['scaledwidth'] = %(#{scaledwidth}%)
          end
        end
      else
        block.caption ||= attributes.delete('caption')
      end
      # TODO eventualy remove the style attribute from the attributes hash
      #block.style     = attributes.delete('style')
      block.style     = attributes['style']
      # AsciiDoc always use [id] as the reftext in HTML output,
      # but I'd like to do better in Asciidoctor
      if (block_id = (block.id ||= attributes['id']))
        # TODO sub reftext
        document.register(:ids, [block_id, (attributes['reftext'] || (block.title? ? block.title : nil))])
      end
      # FIXME remove the need for this update!
      block.attributes.update(attributes) unless attributes.empty?
      block.lock_in_subs

      #if document.attributes.has_key? :pending_attribute_entries
      #  document.attributes.delete(:pending_attribute_entries).each do |entry|
      #    entry.save_to block.attributes
      #  end
      #end

      if block.sub? :callouts
        unless (catalog_callouts block.source, document)
          # No need to sub callouts if they aren't there
          block.remove_sub :callouts
        end
      end
    end

    block
  end