class Fabricator::Vertical_Peeker::Integrator

Attributes

output[R]
section_count[R]

Public Class Methods

new() click to toggle source
Calls superclass method
# File lib/mau/fabricator.rb, line 112
def initialize
  super()
  @output = OpenStruct.new(
    warnings: [],
    presentation: [], # list of titles and sections
    toc: [],
    chunks_by_name: {},
        # canonical_name => {
        #   root_type: String,
        #   chunks: list of :chunk/:diverted_chunk records,
        #   headers: list of :chunk/:divert records,
        # }
    roots: [], # list of canonical names
  )
  @cursec = nil # The current section if started
  @section_count = 0 # The number of last section
  @title_counters = [0]
  @curdivert = nil # The current diversion if active
  @last_divertee = nil
      # last chunk diverted by [[@curdivert]]
  @list_stack = nil
  @in_code = false
  @last_title_level = 0
  @warning_counter = 0
  return
end

Public Instance Methods

check_chunk_sizes(limit) click to toggle source
# File lib/mau/fabricator.rb, line 391
def check_chunk_sizes limit
  return unless limit
  @output.presentation.each do |node|
    next unless node.type == :section
    node.elements.each do |element|
      next unless element.type == :chunk
      if element.lines.length > limit then
        if element.lines.length > limit * 2 then
          assessment, factor = "very long chunk", 2
        else
          assessment, factor = "long chunk", 1
        end
        limit_loc = element.body_loc.dup
        limit_loc.column = nil
        limit_loc.line += limit * factor
        (element.warnings ||= []).push \
            warn(limit_loc, "%s (%i lines)" %
                    [assessment, element.lines.length],
                inline: true)
      end
    end
  end
  return
end
check_root_type_consistency() click to toggle source
# File lib/mau/fabricator.rb, line 95
def check_root_type_consistency
  @output.roots.each do |name|
    cbn_entry = @output.chunks_by_name[name]
    effective_root_type = cbn_entry.root_type
    cbn_entry.headers.each do |element|
      unless element.root_type == effective_root_type then
        warn element.header_loc,
            "inconsistent root type, assuming %s" %
                effective_root_type
      end
    end
  end
  return
end
clear_diversion() click to toggle source
# File lib/mau/fabricator.rb, line 371
def clear_diversion
  if @curdivert then
    if !@last_divertee then
      (@curdivert.warnings ||= []).push \
          warn(@curdivert.header_loc,
              "unused diversion",
              inline: true)
    elsif @last_divertee.initial then
      (@curdivert.warnings ||= []).push \
          warn(@curdivert.header_loc,
              "single-use diversion",
              inline: true)
    end
    @curdivert = nil
    @last_divertee.final = true if @last_divertee
    @last_divertee = nil
  end
  return
end
force_section_break() click to toggle source
# File lib/mau/fabricator.rb, line 364
def force_section_break
  @cursec = nil
  @list_stack = nil
  @in_code = false
  return
end
integrate(element) click to toggle source
# File lib/mau/fabricator.rb, line 139
def integrate element
  if element.type == :title then
    # Check the title's level restriction
    if element.level > @last_title_level + 1 then
      warn element.loc, "title level too deep"
      element.level = @last_title_level + 1
    end
    @last_title_level = element.level

    # Number the title
    while @title_counters.length > element.level do
      @title_counters.pop
    end
    if @title_counters.length < element.level then
      @title_counters.push 0
    end
    @title_counters[-1] += 1
    element.number = @title_counters.join '.'

    # Append the node to [[presentation]] and [[toc]]
    force_section_break
    @output.presentation.push element
    @output.toc.push element

    # Enforce (sub(sub))chapter-locality of diversions
    clear_diversion
  else
    if element.type == :block and @curdivert then
      element.type = :diverted_chunk
      element.name = @curdivert.name
      element.divert = @curdivert

      element.initial = true if @last_divertee.nil?
      @last_divertee = element
    end
    if [:divert, :chunk].include? element.type then
      clear_diversion
    end
    if (@cursec and element.type == :rubric) or
        (@in_code and
            [:paragraph, :block, :item].include?(
                element.type)) then
      (@cursec.warnings ||= []).push \
          warn(element.loc,
              "silent section break",
              inline: true)
      force_section_break
    end
    if @cursec.nil? then
      @cursec = OpenStruct.new(
        type: :section,
        section_number: (@section_count += 1),
        elements: [])
      @output.presentation.push @cursec
    end
    if element.type == :rubric then
      element.section_number = @cursec.section_number
      @output.toc.push element
    end
    if element.type == :divert then
      @curdivert = element
      raise 'assertion failed' unless @last_divertee.nil?
    end

    if element.type == :item then
      # Is this a top-level or descendant item?
      unless @list_stack then
        raise 'assertion failed' unless element.indent == 0

        # Create a new [[list]] node.
        new_list = OpenStruct.new(
          type: :list,
          items: [],
          indent: element.indent)
        @cursec.elements.push new_list
        @list_stack = [new_list]
      else
        while @list_stack.last.indent > element.indent do
          if @list_stack[-2].indent < element.indent then
            # Unexpected de-dent, like this:
            #    - master list
            #         - child 1
            #       - child 2
            @list_stack.last.indent = element.indent
            (element.warnings ||= []).push \
                warn(element.loc,
                    "unexpected dedent", inline: true)
            break
          end
          @list_stack.pop
        end
        if @list_stack.last.indent < element.indent then
          if @list_stack.last.sublist then
            raise 'assertion failed'
          end
          new_list = OpenStruct.new(
            type: :list,
            items: [],
            indent: element.indent)
          @list_stack.last.items.last.sublist = new_list
          @list_stack.push new_list
        end
      end

      # The list structure has been prepared.  Append the
      # new element to the innermost list in progress.
      @list_stack.last.items.push element
    else
      @cursec.elements.push element

      if [:chunk, :diverted_chunk].
          include?(element.type) then
        element.section_number = @cursec.section_number
        @in_code = true
            # so we can generate a section break if a
            # narrative-type element follows
        element.content = []
        element.lines.each_with_index do
            |line, lineno_in_chunk|
          unless lineno_in_chunk.zero? then
            element.content.push \
                OpenStruct.new(type: :newline)
          end
          column = 1 + element.indent
          line.split(/(<<\s*
              (?:
               \[\[.*?\]*\]\]
               | .
              )+?
              \s*>>)/x, -1).each_with_index do
                |raw_piece, piece_index|
            node = nil
            if piece_index.odd? then
              name = raw_piece[2 ... -2].strip
                  # discard the surrounding double brokets
                  # together with adjacent whitespace
              node = OpenStruct.new(type: :use,
                  name: nil,
                      # for ordering; will be replaced below
                  raw: raw_piece,
                  loc: OpenStruct.new(
                      filename: element.body_loc.filename,
                      line: element.body_loc.line +
                          lineno_in_chunk,
                      column: column)
              )
              if name =~ /(?:^|\s+)(\|[\w>-]+)$/ and
                  Fabricator::POSTPROCESSES.has_key? $1 then
                node.postprocess = $1; name = $`
              end
              if name =~ /(?:^|\s+)(\.dense)$/ then
                node.vertical_separation = $1; name = $`
              end
              if name =~ /^(\.clearindent)(?:\s+|$)/ then
                node.clearindent = true; name = $'
              end
              if !name.empty? then
                node.name =
                    Fabricator.canonicalise_chunk_name(name)
              else
                # not a proper reference, after all
                node = nil
              end
              # If failed, [[node]] is still [[nil]].
            end
            if node.nil? and !raw_piece.empty? then
              node = OpenStruct.new(
                type: :verbatim,
                data: raw_piece)
            end
            element.content.push node if node
            column += raw_piece.length
          end
        end
      end
      if [:chunk, :diverted_chunk, :divert].include?(
          element.type) then
        cbn_record =
            @output.chunks_by_name[element.name] ||=
                OpenStruct.new(chunks: [], headers: [])
        if [:chunk, :diverted_chunk].include?(
            element.type) then
          cbn_record.chunks.push element
        end
        if [:chunk, :divert].include? element.type then
          cbn_record.headers.push element
        end

        if element.root_type then
          # check the filename's reasonability
          bad_name = false
          parts = element.name.split '/'
          if ['', '.', '..'].any?{|d| parts.include? d} then
            bad_name = true
          end
          unless parts.all?{|p| p =~ /\A[\w.-]+\Z/} then
            bad_name = true
          end
          if bad_name then
            (element.warnings ||= []).push \
                warn(element.header_loc,
                    "unuseable filename",
                    inline: true)
            element.root_type = nil
          end
        end

        # The :chunks_by_name record will hold the highest
        # root_type for chunks of this name, with the order
        # defined as [[nil]] < [['.file']] < [['.script']].
        if element.root_type and
            cbn_record.root_type.nil? then
          cbn_record.root_type = element.root_type
          @output.roots.push element.name
        end
        if element.root_type == '.script' then
          cbn_record.root_type = element.root_type
        end
      end
      @list_stack = nil
    end
  end
  return
end
tangle_chunks(cbn_entry, sink, trace, vsep = 2) click to toggle source
# File lib/mau/fabricator.rb, line 426
def tangle_chunks cbn_entry, sink, trace, vsep = 2
  chain_start_loc = nil
  cbn_entry.chunks.each_with_index do |chunk, i|
    vsep.times{sink.newline} unless i.zero?
    if chunk.divert and chunk.initial then
      raise 'assertion failed' if chain_start_loc
      chain_start_loc = sink.location_ahead
    end
    start_location = sink.location_ahead
    chunk.content.each do |node|
      case node.type
      when :verbatim then
        sink.write node.data
      when :newline then
        sink.newline
      when :use then
        tangle_transclusion node, sink, trace, chunk
      else raise 'data structure error'
      end
    end
    end_location = sink.location_behind

    # Both endpoints are inclusive.
    (chunk.tangle_locs ||= []).push OpenStruct.new(
      from: start_location,
      to: end_location)
    if chunk.divert and chunk.final then
      raise 'assertion failed' unless chain_start_loc
      (chunk.divert.chain_tangle_locs ||= []).push \
          OpenStruct.new(
              from: chain_start_loc,
              to: sink.location_behind)
      chain_start_loc = nil
    end
  end
  return
end
tangle_roots() click to toggle source
# File lib/mau/fabricator.rb, line 509
def tangle_roots
  return if @output.tangles
  @output.tangles = {}
  @output.roots.each do |name|
    sport = StringIO.new
    sink = Fabricator::Tangling_Sink.new name, sport
    cbn_entry = @output.chunks_by_name[name]
    # We can assume that [[cbn_entry]] is not [[nil]], for
    # otherwise there wouldn't be a [[roots]] entry.
    tangle_chunks cbn_entry, sink, Set.new([name])
    sink.newline
    @output.tangles[name] = OpenStruct.new(
      filename: name,
      root_type: cbn_entry.root_type,
      content: sport.string,
      line_count: sink.line_count,
      nonblank_line_count: sink.nonblank_line_count,
      longest_line_length: sink.longest_line_length,
    )
  end
  return
end
tangle_transclusion(node, sink, trace, referrer) click to toggle source
# File lib/mau/fabricator.rb, line 464
def tangle_transclusion node, sink, trace, referrer
  name = node.name
  if trace.include? name then
    warn node.loc, "circular reference"
    sink.write node.raw
  else
    cbn_entry = @output.chunks_by_name[name]
    if cbn_entry.nil? or cbn_entry.chunks.empty? then
      warn node.loc, "dangling reference"
      sink.write node.raw
    else
      (cbn_entry.transcluders ||= []).push(
          OpenStruct.new(
            name: referrer.name,
            section_number: referrer.section_number,
            ))
      trace.add name
      if node.postprocess then
        # redirect the tangler
        outer_sink = sink
        inner_sport = StringIO.new
        sink = Fabricator::Tangling_Sink.new '(pipe)',
            inner_sport
      end
      sink.pin_indent node.clearindent ? 0 : nil do
        tangle_chunks cbn_entry, sink, trace,
            node.vertical_separation == '.dense' ? 1 : 2
      end
      if node.postprocess then
        # revert the redirect and apply the filter
        sink.newline
        filter_output =
            Fabricator::POSTPROCESSES[node.postprocess].
            call(inner_sport.string)
        sink = outer_sink
        sink.pin_indent node.clearindent ? 0 : nil do
          sink.write_long filter_output
        end
      end
      trace.delete name
    end
  end
  return
end
warn(location, message, inline: false) click to toggle source
# File lib/mau/fabricator.rb, line 416
def warn location, message, inline: false
  record = OpenStruct.new(
    loc: location,
    message: message,
    number: @warning_counter += 1,
    inline: inline)
  @output.warnings.push record
  return record # so it can also be attached elsewhere
end