lib/asciidoctor/abstract_block.rb



# frozen_string_literal: true
module Asciidoctor
class AbstractBlock < AbstractNode
  # Public: Get the Array of {AbstractBlock} child blocks for this block. Only applies if content model is :compound.
  attr_reader :blocks

  # Public: Set the caption for this block.
  attr_writer :caption

  # Public: Describes the type of content this block accepts and how it should be converted. Acceptable values are:
  # * :compound - this block contains other blocks
  # * :simple - this block holds a paragraph of prose that receives normal substitutions
  # * :verbatim - this block holds verbatim text (displayed "as is") that receives verbatim substitutions
  # * :raw - this block holds unprocessed content passed directly to the output with no sustitutions applied
  # * :empty - this block has no content
  attr_accessor :content_model

  # Public: Set the Integer level of this {Section} or the level of the Section to which this {AbstractBlock} belongs.
  attr_accessor :level

  # Public: Get/Set the String numeral of this block (if section, relative to parent, otherwise absolute).
  # Only assigned to section if automatic section numbering is enabled.
  # Only assigned to formal block (block with title) if corresponding caption attribute is present.
  attr_accessor :numeral

  # Public: Gets/Sets the location in the AsciiDoc source where this block begins.
  attr_accessor :source_location

  # Public: Get/Set the String style (block type qualifier) for this block.
  attr_accessor :style

  # Public: Substitutions to be applied to content in this block.
  attr_reader :subs

  def initialize parent, context, opts = {}
    super
    @content_model = :compound
    @blocks = []
    @subs = []
    @id = @title = @caption = @numeral = @style = @default_subs = @source_location = nil
    case context
    when :document, :section
      @level = @next_section_index = 0
      @next_section_ordinal = 1
    else
      @level = AbstractBlock === parent ? parent.level : nil
    end
  end

  def block?
    true
  end

  def inline?
    false
  end

  # Public: Get the source file where this block started
  def file
    @source_location && @source_location.file
  end

  # Public: Get the source line number where this block started
  def lineno
    @source_location && @source_location.lineno
  end

  # Public: Get the converted String content for this Block.  If the block
  # has child blocks, the content method should cause them to be
  # converted and returned as content that can be included in the
  # parent block's template.
  def convert
    @document.playback_attributes @attributes
    converter.convert self
  end

  # Deprecated: Use {AbstractBlock#convert} instead.
  alias render convert

  # Public: Get the converted result of the child blocks by converting the
  # children appropriate to content model that this block supports.
  def content
    @blocks.map {|b| b.convert }.join LF
  end

  # Public: Update the context of this block.
  #
  # This method changes the context of this block. It also updates the node name accordingly.
  #
  # context - the context Symbol context to assign to this block
  #
  # Returns the new context Symbol assigned to this block
  def context= context
    @node_name = (@context = context).to_s
  end

  # Public: Append a content block to this block's list of blocks.
  #
  # block - The new child block.
  #
  # Examples
  #
  #   block = Block.new(parent, :preamble, content_model: :compound)
  #
  #   block << Block.new(block, :paragraph, source: 'p1')
  #   block << Block.new(block, :paragraph, source: 'p2')
  #   block.blocks?
  #   # => true
  #   block.blocks.size
  #   # => 2
  #
  # Returns The parent Block
  def << block
    block.parent = self unless block.parent == self
    @blocks << block
    self
  end

  # NOTE append alias required for adapting to a Java API
  alias append <<

  # Public: Determine whether this Block contains block content
  #
  # Returns A Boolean indicating whether this Block has block content
  def blocks?
    @blocks.empty? ? false : true
  end

  # Public: Check whether this block has any child Section objects.
  #
  # Only applies to Document and Section instances
  #
  # Returns A [Boolean] to indicate whether this block has child Section objects
  def sections?
    @next_section_index > 0
  end

  # Deprecated: Legacy property to get the String or Integer numeral of this section.
  def number
    (Integer @numeral) rescue @numeral
  end

  # Public: Walk the document tree and find all block-level nodes that match the specified selector (context, style, id,
  # role, and/or custom filter).
  #
  # If a Ruby block is given, it's applied as a supplemental filter. If the filter returns true (which implies :accept),
  # the node is accepted and node traversal continues. If the filter returns false (which implies :skip), the node is
  # skipped, but its children are still visited. If the filter returns :reject, the node and all its descendants are
  # rejected. If the filter returns :prune, the node is accepted, but its descendants are rejected. If no selector
  # or filter block is supplied, all block-level nodes in the tree are returned.
  #
  # Examples
  #
  #   doc.find_by context: :section
  #   #=> Asciidoctor::Section@14459860 { level: 0, title: "Hello, AsciiDoc!", blocks: 0 }
  #   #=> Asciidoctor::Section@14505460 { level: 1, title: "First Section", blocks: 1 }
  #
  #   doc.find_by(context: :section) {|section| section.level == 1 }
  #   #=> Asciidoctor::Section@14505460 { level: 1, title: "First Section", blocks: 1 }
  #
  #   doc.find_by context: :listing, style: 'source'
  #   #=> Asciidoctor::Block@13136720 { context: :listing, content_model: :verbatim, style: "source", lines: 1 }
  #
  # Returns An Array of block-level nodes that match the filter or an empty Array if no matches are found
  #--
  # TODO support jQuery-style selector (e.g., image.thumb)
  def find_by selector = {}, &block
    find_by_internal selector, (result = []), &block
  rescue ::StopIteration
    result
  end

  alias query find_by

  # Move to the next adjacent block in document order. If the current block is the last
  # item in a list, this method will return the following sibling of the list block.
  def next_adjacent_block
    unless @context == :document
      if (p = @parent).context == :dlist && @context == :list_item
        (sib = p.items[(p.items.find_index {|terms, desc| (terms.include? self) || desc == self }) + 1]) ? sib : p.next_adjacent_block
      else
        (sib = p.blocks[(p.blocks.find_index self) + 1]) ? sib : p.next_adjacent_block
      end
    end
  end

  # Public: Get the Array of child Section objects
  #
  # Only applies to Document and Section instances
  #
  # Examples
  #
  #   doc << (sect1 = Section.new doc, 1)
  #   sect1.title = 'Section 1'
  #   para1 = Block.new sect1, :paragraph, source: 'Paragraph 1'
  #   para2 = Block.new sect1, :paragraph, source: 'Paragraph 2'
  #   sect1 << para1 << para2
  #   sect1 << (sect1_1 = Section.new sect1, 2)
  #   sect1_1.title = 'Section 1.1'
  #   sect1_1 << (Block.new sect1_1, :paragraph, source: 'Paragraph 3')
  #   sect1.blocks?
  #   # => true
  #   sect1.blocks.size
  #   # => 3
  #   sect1.sections.size
  #   # => 1
  #
  # Returns an [Array] of Section objects
  def sections
    @blocks.select {|block| block.context == :section }
  end

  # Public: Returns the converted alt text for this block image.
  #
  # Returns the [String] value of the alt attribute with XML special character
  # and replacement substitutions applied.
  def alt
    if (text = @attributes['alt'])
      if text == @attributes['default-alt']
        sub_specialchars text
      else
        text = sub_specialchars text
        (ReplaceableTextRx.match? text) ? (sub_replacements text) : text
      end
    end
  end

  # Gets the caption for this block.
  #
  # This method routes the deprecated use of the caption method on an
  # admonition block to the textlabel attribute.
  #
  # Returns the [String] caption for this block (or the value of the textlabel
  # attribute if this is an admonition block).
  def caption
    @context == :admonition ? @attributes['textlabel'] : @caption
  end

  # Public: Convenience method that returns the interpreted title of the Block
  # with the caption prepended.
  #
  # Concatenates the value of this Block's caption instance variable and the
  # return value of this Block's title method. No space is added between the
  # two values. If the Block does not have a caption, the interpreted title is
  # returned.
  #
  # Returns the converted String title prefixed with the caption, or just the
  # converted String title if no caption is set
  def captioned_title
    %(#{@caption}#{title})
  end

  # Public: Retrieve the list marker keyword for the specified list type.
  #
  # For use in the HTML type attribute.
  #
  # list_type - the type of list; default to the @style if not specified
  #
  # Returns the single-character [String] keyword that represents the marker for the specified list type
  def list_marker_keyword list_type = nil
    ORDERED_LIST_KEYWORDS[list_type || @style]
  end

  # Public: Get the String title of this Block with title substitions applied
  #
  # The following substitutions are applied to block and section titles:
  #
  # :specialcharacters, :quotes, :replacements, :macros, :attributes and :post_replacements
  #
  # Examples
  #
  #   block.title = "Foo 3^ # {two-colons} Bar(1)"
  #   block.title
  #   => "Foo 3^ # :: Bar(1)"
  #
  # Returns the converted String title for this Block, or nil if the source title is falsy
  def title
    # prevent substitutions from being applied to title multiple times
    @converted_title ||= @title && (apply_title_subs @title)
  end

  # Public: A convenience method that checks whether the title of this block is defined.
  #
  # Returns a [Boolean] indicating whether this block has a title.
  def title?
    @title ? true : false
  end

  # Public: Set the String block title.
  #
  # Returns the new String title assigned to this Block
  def title= val
    @converted_title = nil
    @title = val
  end

  # Public: A convenience method that checks whether the specified
  # substitution is enabled for this block.
  #
  # name - The Symbol substitution name
  #
  # Returns A Boolean indicating whether the specified substitution is
  # enabled for this block
  def sub? name
    @subs.include? name
  end

  # Public: Remove a substitution from this block
  #
  # sub  - The Symbol substitution name
  #
  # Returns nothing
  def remove_sub sub
    @subs.delete sub
    nil
  end

  # Public: Generate cross reference text (xreftext) that can be used to refer
  # to this block.
  #
  # Use the explicit reftext for this block, if specified, retrieved from the
  # {#reftext} method. Otherwise, if this is a section or captioned block (a
  # block with both a title and caption), generate the xreftext according to
  # the value of the xrefstyle argument (e.g., full, short). This logic may
  # leverage the {Substitutors#sub_quotes} method to apply formatting to the
  # text. If this is not a captioned block, return the title, if present, or
  # nil otherwise.
  #
  # xrefstyle - An optional String that specifies the style to use to format
  #             the xreftext ('full', 'short', or 'basic') (default: nil).
  #
  # Returns the generated [String] xreftext used to refer to this block or
  # nothing if there isn't sufficient information to generate one.
  def xreftext xrefstyle = nil
    if (val = reftext) && !val.empty?
      val
    # NOTE xrefstyle only applies to blocks with a title and a caption or number
    elsif xrefstyle && @title && @caption
      case xrefstyle
      when 'full'
        quoted_title = sub_placeholder (sub_quotes @document.compat_mode ? %q(``%s'') : '"`%s`"'), title
        if @numeral && (caption_attr_name = CAPTION_ATTR_NAMES[@context]) && (prefix = @document.attributes[caption_attr_name])
          %(#{prefix} #{@numeral}, #{quoted_title})
        else
          %(#{@caption.chomp '. '}, #{quoted_title})
        end
      when 'short'
        if @numeral && (caption_attr_name = CAPTION_ATTR_NAMES[@context]) && (prefix = @document.attributes[caption_attr_name])
          %(#{prefix} #{@numeral})
        else
          @caption.chomp '. '
        end
      else # 'basic'
        title
      end
    else
      title
    end
  end

  # Public: Generate and assign caption to block if not already assigned.
  #
  # If the block has a title and a caption prefix is available for this block,
  # then build a caption from this information, assign it a number and store it
  # to the caption attribute on the block.
  #
  # If a caption has already been assigned to this block, do nothing.
  #
  # The parts of a complete caption are: <prefix> <number>. <title>
  # This partial caption represents the part the precedes the title.
  #
  # value           - The explicit String caption to assign to this block (default: nil).
  # caption_context - The Symbol context to use when resolving caption-related attributes.
  #                   If not provided, the name of the context for this block is used.
  #                   (default: @context)
  #
  # Returns nothing.
  def assign_caption value = nil, caption_context = @context
    unless @caption || !@title || (@caption = value || @document.attributes['caption'])
      if (attr_name = CAPTION_ATTR_NAMES[caption_context]) && (prefix = @document.attributes[attr_name])
        @caption = %(#{prefix} #{@numeral = @document.increment_and_store_counter %(#{caption_context}-number), self}. )
        nil
      end
    end
  end

  # Internal: Assign the next index (0-based) and numeral (1-based) to the section.
  # If the section is an appendix, the numeral is a letter (starting with A). This
  # method also assigns the appendix caption.
  #
  # section - The section to which to assign the next index and numeral.
  #
  # Assign to the specified section the next index and, if the section is
  # numbered, the numeral within this block (its parent).
  #
  # Returns nothing
  def assign_numeral section
    @next_section_index = (section.index = @next_section_index) + 1
    if (like = section.numbered)
      if (sectname = section.sectname) == 'appendix'
        section.numeral = @document.counter 'appendix-number', 'A'
        section.caption = (caption = @document.attributes['appendix-caption']) ? %(#{caption} #{section.numeral}: ) : %(#{section.numeral}. )
      # NOTE currently chapters in a book doctype are sequential even for multi-part books (see #979)
      elsif sectname == 'chapter' || like == :chapter
        section.numeral = (@document.counter 'chapter-number', 1).to_s
      else
        section.numeral = sectname == 'part' ? (Helpers.int_to_roman @next_section_ordinal) : @next_section_ordinal.to_s
        @next_section_ordinal += 1
      end
    end
    nil
  end

  # Internal: Reassign the section indexes
  #
  # Walk the descendents of the current Document or Section
  # and reassign the section 0-based index value to each Section
  # as it appears in document order.
  #
  # IMPORTANT You must invoke this method on a node after removing
  # child sections or else the internal counters will be off.
  #
  # Returns nothing
  def reindex_sections
    @next_section_index = 0
    @next_section_ordinal = 1
    @blocks.each do |block|
      if block.context == :section
        assign_numeral block
        block.reindex_sections
      end
    end
  end

  protected

  # Internal: Performs the work for find_by, but does not handle the StopIteration exception.
  def find_by_internal selector = {}, result = [], &block
    if ((any_context = (context_selector = selector[:context]) ? nil : true) || context_selector == @context) &&
        (!(style_selector = selector[:style]) || style_selector == @style) &&
        (!(role_selector = selector[:role]) || (has_role? role_selector)) &&
        (!(id_selector = selector[:id]) || id_selector == @id)
      if block_given?
        if (verdict = yield self)
          case verdict
          when :prune
            result << self
            raise ::StopIteration if id_selector
            return result
          when :reject
            raise ::StopIteration if id_selector
            return result
          when :stop
            raise ::StopIteration
          else
            result << self
            raise ::StopIteration if id_selector
          end
        elsif id_selector
          raise ::StopIteration
        end
      else
        result << self
        raise ::StopIteration if id_selector
      end
    end
    case @context
    when :document
      unless context_selector == :document
        # process document header as a section, if present
        if header? && (any_context || context_selector == :section)
          @header.find_by_internal selector, result, &block
        end
        @blocks.each do |b|
          next if (context_selector == :section && b.context != :section) # optimization
          b.find_by_internal selector, result, &block
        end
      end
    when :dlist
      # dlist has different structure than other blocks
      if any_context || context_selector != :section # optimization
        # NOTE the list item of a dlist can be nil, so we have to check
        @blocks.flatten.each {|b| b.find_by_internal selector, result, &block if b }
      end
    when :table
      if selector[:traverse_documents]
        rows.head.each {|r| r.each {|c| c.find_by_internal selector, result, &block } }
        selector = selector.merge context: :document if context_selector == :inner_document
        (rows.body + rows.foot).each do |r|
          r.each do |c|
            c.find_by_internal selector, result, &block
            c.inner_document.find_by_internal selector, result, &block if c.style == :asciidoc
          end
        end
      else
        (rows.head + rows.body + rows.foot).each {|r| r.each {|c| c.find_by_internal selector, result, &block } }
      end
    else
      @blocks.each do |b|
        next if (context_selector == :section && b.context != :section) # optimization
        b.find_by_internal selector, result, &block
      end
    end
    result
  end
end
end