class Litbuild::AsciiDocVisitor

This class writes AsciiDoc fragments to directories, rooted at the DOCUMENT_DIR directory. It adds a sequential number prefix on each fragment written to a directory, and ensures that no duplicate fragments are written: if a fragment for a specific blueprint (or blueprint phase) has already been written to any directory, AsciiDocVisitor will ignore subsequent requests to write that blueprint (phase). AsciiDocVisitor can also write a top-level AsciiDoc document that includes all top-level fragments.

Public Class Methods

new(parameters:) click to toggle source
Calls superclass method
# File lib/litbuild/ascii_doc_visitor.rb, line 18
def initialize(parameters:)
  @parameters = parameters
  super(directory: @parameters['DOCUMENT_DIR'])
  @written = Hash.new { |hash, key| hash[key] = [] }
  @all_written_targets = []
end

Public Instance Methods

visit_commands(commands:) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 25
def visit_commands(commands:)
  file_collector = {}
  extra_section = proc do |doc|
    render_files(doc, commands, file_collector)
  end
  write(blueprint: commands,
        location: cwd,
        extra: extra_section) do |doc, directive, values|
    case directive
    when 'commands' then write_commands(doc, values)
    when 'file' then write_file_chunk(doc, file_collector, values)
    end
  end
end
visit_narrative(narrative:) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 40
def visit_narrative(narrative:)
  write(blueprint: narrative, location: cwd)
end
visit_package(package:) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 44
def visit_package(package:)
  write(blueprint: package,
        location: cwd,
        summary: summary_block_for(package)) do |doc, directive, value|
    case directive
    when 'configure-commands'
      write_stage_commands(doc, value, 'Configuration')
    when 'compile-commands'
      write_stage_commands(doc, value, 'Compilation')
    when 'test-commands'
      write_stage_commands(doc, value, 'Test')
    when 'install-commands'
      write_stage_commands(doc, value, 'Installation')
    when 'before-build-as-root'
      write_stage_commands(doc, value, 'Pre-build (as `root`)')
    when 'after-install-as-root'
      write_stage_commands(doc, value, 'Post-installation (as `root`)')
    when 'patches'
      write_patch_block(doc, package, value)
    when 'in-tree-sources'
      write_in_tree_block(doc, package, value)
    when 'build-dir'
      write_build_dir(doc, value)
    end
  end
end
visit_section(section:) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 71
def visit_section(section:)
  # The files that need to be included by the section -- explicitly
  # declared, added for dependencies, implicitly included by phase
  # or whatever. Explicit blueprints (and their dependencies, if
  # any) will be written into the narrative wherever they appear;
  # the others will be written at the end by
  # `write_remaining_blueprints`.
  files = @written[File.join(cwd, section.name)].clone
  write_remaining_blueprints = proc do |doc|
    files.each do |file|
      doc.puts
      doc.puts("include::./#{section.name}/#{file}[leveloffset=+1]")
    end
  end
  write(blueprint: section,
        location: cwd,
        extra: write_remaining_blueprints) do |doc, directive, value|
    case directive
    when 'blueprints'
      write_blueprint_includes(doc, value, section.name, files)
    when 'package-list-with-versions'
      write_package_list(doc, section)
    end
  end
end
write_toplevel_doc(blueprint:) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 97
def write_toplevel_doc(blueprint:)
  doc_dir = @parameters['DOCUMENT_DIR']
  return if @written[doc_dir].empty?

  doc_name = File.join(doc_dir, "#{blueprint.file_name}.adoc")
  File.open(doc_name, 'w') do |f|
    top_header = blueprint.header_text
    top_header += " (#{blueprint.active_phase})" if blueprint.active_phase
    f.puts("= #{top_header}")
    f.puts(blueprint.value('author')) if blueprint['author']
    f.puts(blueprint.value('revision')) if blueprint['revision']
    f.puts(':sectnums:')
    f.puts(':doctype: book') unless other_parts.empty?
    write_toplevel_includes(location: doc_dir, document: f)
  end
end

Private Instance Methods

dependency_anchor_list(dep_directives) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 322
def dependency_anchor_list(dep_directives)
  anchors = dep_directives.map { |d| "<<#{d}>>" }
  anchors.join(', ')
end
handle_directives(doc, dir_hash, block) click to toggle source

Render some AsciiDoc for a directive hash into the specified document. Any directive that may be specified as part of any blueprint should be handled directly here. Other types of directives will be passed into the block given originally to the `write` method; each visit method may define a block that does the correct thing for all directives that may be defined in that type of blueprint.

# File lib/litbuild/ascii_doc_visitor.rb, line 209
def handle_directives(doc, dir_hash, block)
  dir_hash.each do |directive, values|
    if directive == 'parameter'
      write_parameter(doc, values)
    elsif directive == 'environment'
      write_environment(doc, values)
    elsif directive == 'depends-on'
      write_dependencies(doc, values)
    elsif directive == 'configuration-files'
      write_cfgfiles(doc, values)
    elsif directive == 'servicedir'
      write_servicedir(doc, values)
    elsif directive == 'service-pipeline'
      write_servicepipe(doc, values)
    elsif block
      block.call(doc, directive, values)
    end
  end
end
phase_heading(level, blueprint) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 186
def phase_heading(level, blueprint)
  "[[#{blueprint.name_with_phase},#{blueprint.header_text_with_phase}]]\n" \
  "#{level} #{blueprint.header_text_with_phase}\n\n"
end
render_base_grafs?(blueprint) click to toggle source

Should the base grafs of the blueprint be rendered? This is always true for unphased blueprints (since there is nothing else to render) and is true the first time a phased blueprint is rendered.

# File lib/litbuild/ascii_doc_visitor.rb, line 155
def render_base_grafs?(blueprint)
  return true unless blueprint.phases?

  @all_written_targets.none? { |target| target =~ /^#{blueprint.name}::/ }
end
render_files(doc, blueprint, files) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 366
def render_files(doc, blueprint, files)
  return if files.empty?

  doc.puts("[[#{blueprint.name_with_phase}_files]]")
  doc.puts("== Complete text of files\n\n")
  files.keys.sort.each do |filename|
    doc.puts("=== #{filename}\n\n")
    doc.puts('[source,indent=0]')
    doc.puts('----')
    doc.puts(files[filename])
    doc.puts('----')
  end
end
render_grafs(doc, grafs, block) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 191
def render_grafs(doc, grafs, block)
  grafs.each do |graf|
    case graf
    when String then doc.puts(graf)
    when Hash then handle_directives(doc, graf, block)
    end
    doc.puts
  end
end
summary_block_for(package) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 380
def summary_block_for(package)
  proc do |doc, for_phase|
    write_full_block = !package.phases? || !for_phase
    next unless write_full_block || package.build_dir ||
                package.directives['environment']

    doc.puts("[%autowidth,cols=\"h,d\"]\n|===\n\n")
    if write_full_block
      doc.puts("|Name|#{package.full_name}\n|Version|#{package.version}")
      write_url_rows(doc, package)
      write_list_row(doc, 'Patches', package.patch_files) do |pf|
        "- `#{pf}`"
      end
      write_list_row(doc, 'Built In-Tree', package.in_tree) do |pkg|
        "- #{pkg[0]} #{pkg[1]}"
      end
      write_dependency_row(doc, package)
    end
    if !package.phases? || for_phase
      write_environment_row(doc, package)
      if package.build_dir
        doc.puts("|Build Directory| `#{package.build_dir}`")
      end
    end
    doc.puts("|===\n\n")
  end
end
to_asciidoc(blueprint, extra, summary, block) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 161
def to_asciidoc(blueprint, extra, summary, block)
  doc = StringIO.new
  if render_base_grafs?(blueprint)
    doc.puts("[[#{blueprint.name},#{blueprint.header_text}]]")
    doc.puts("= #{blueprint.header_text}")
    doc.puts
    summary.call(doc, false)
    doc.puts("== Overview\n\n") if blueprint.active_phase
    render_grafs(doc, blueprint.base_grafs, block)
    doc.puts(phase_heading('==', blueprint)) if blueprint.active_phase
  else
    doc.puts(phase_heading('=', blueprint))
    nm = blueprint.name
    doc.puts("_For an overview of #{nm}, see <<#{nm}>>._\n\n")
  end
  if blueprint.active_phase
    summary.call(doc, true)
    render_grafs(doc, blueprint.phase_grafs, block)
  end
  extra.call(doc)
  # If there are any extraneous blank lines in the document, get rid
  # of them.
  doc.string.gsub(/\n\n\n+/, "\n\n").strip + "\n"
end
write(blueprint:, location:, extra: proc {}, summary: proc {}, &block) click to toggle source

Write an AsciiDoc fragment.

blueprint: the blueprint for which to write a fragment. location: the directory where the fragment should be written. extra: a proc that will render extra sections to be added at the

end, if any.

summary: a proc that will render a summary block, if one is

needed. This will be called with the document I/O and with a
`for_phase` parameter of `false` when writing an unphased
blueprint or the base narrative for a phased blueprint, or
`true` if writing the phase narrative of a phased blueprint.

This method should be called with a block that accepts:

- the document I/O, which may be written to as needed;
- the name of a directive being rendered; and
- the directive values.

The block should render whatever AsciiDoc is suitable based on the directives passed to it. If a directive need not result in any output, just ignore it.

# File lib/litbuild/ascii_doc_visitor.rb, line 136
def write(blueprint:, location:, extra: proc {}, summary: proc {}, &block)
  return if @all_written_targets.include?(blueprint.target_name)

  FileUtils.mkdir_p(location)
  doc_name = format('%<count>02d-%<doc>s',
                    count: @written[location].size,
                    doc: blueprint.file_name + '.adoc')
  File.open(File.join(location, doc_name), 'w') do |f|
    f.write(to_asciidoc(blueprint, extra, summary, block))
  end
  @written[location] << doc_name
  @all_written_targets << blueprint.target_name
end
write_blueprint_includes(doc, blueprints, section_name, files) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 478
def write_blueprint_includes(doc, blueprints, section_name, files)
  until blueprints.empty?
    bp = blueprints.shift
    bpfile = bp.sub('::', '-').tr(' ', '-')
    rendered_bpfile = files.detect { |f| f =~ /[0-9]-#{bpfile}.adoc/ }
    unless rendered_bpfile
      msg = "specifies blueprint #{bp} but it has already been rendered"
      raise(Litbuild::UnrenderedComponent, "Section #{section_name} #{msg}")
    end
    loop do
      to_render = files.shift
      doc.puts("include::./#{section_name}/#{to_render}[leveloffset=+1]")
      doc.puts
      break if to_render == rendered_bpfile
    end
  end
end
write_build_dir(doc, build_dir) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 338
def write_build_dir(doc, build_dir)
  doc.puts("Build Directory:: `#{build_dir.first}`")
end
write_cfgfiles(doc, files) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 331
def write_cfgfiles(doc, files)
  doc.puts('.Configuration Files')
  files.each do |file|
    doc.puts("- `#{file}`")
  end
end
write_commands(doc, commands) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 342
def write_commands(doc, commands)
  doc.puts('.Commands:')
  doc.puts('[source,bash]')
  doc.puts('----')
  commands.each { |cmd| doc.puts(cmd) }
  doc.puts('----')
end
write_dependencies(doc, dep_directives) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 327
def write_dependencies(doc, dep_directives)
  doc.puts("Dependencies:: #{dependency_anchor_list(dep_directives)}.")
end
write_dependency_row(doc, blueprint) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 408
def write_dependency_row(doc, blueprint)
  dependencies = blueprint.deduped_dependency_names
  return if dependencies.empty?

  doc.puts("|Dependencies|#{dependency_anchor_list(dependencies)}")
end
write_environment(doc, env_directives) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 309
def write_environment(doc, env_directives)
  env_directives.each do |variables|
    variables.each do |varname, value|
      env_val = if value.first.empty?
                  '_(should not be set)_'
                else
                  "`#{value.first}`"
                end
      doc.puts("Environment variable: #{varname}:: #{env_val}")
    end
  end
end
write_environment_row(doc, package) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 415
def write_environment_row(doc, package)
  return unless package['environment']

  doc.puts('|Environment')
  doc.puts('a|')
  package['environment'].each do |env_hash|
    env_hash.each do |var, value|
      if value.first.empty?
        doc.puts("- unset `#{var}`")
      else
        doc.puts("- `#{var}`: `#{value.first}`")
      end
    end
  end
  doc.puts
end
write_file_chunk(doc, file_collector, file_directive) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 350
def write_file_chunk(doc, file_collector, file_directive)
  file = file_directive.first
  filename = file['name'].first
  if file_collector.key?(filename)
    doc.puts(".File #{filename} (continued):")
    file_collector[filename] = file_collector[filename] + file['content']
  else
    doc.puts(".File #{filename}:")
    file_collector[filename] = file['content']
  end
  doc.puts('[source,indent=0]')
  doc.puts('----')
  doc.puts(file['content'])
  doc.puts('----')
end
write_in_tree_block(doc, package, intree_to_render) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 470
def write_in_tree_block(doc, package, intree_to_render)
  doc.puts('.In-Tree Sources:')
  intree_to_render.each do |intree|
    package, version, path = intree.split
    doc.puts("- #{package} #{version} (at `#{path}`)")
  end
end
write_list_row(doc, header, items) { |pf| ... } click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 432
def write_list_row(doc, header, items)
  return if items.nil? || items.empty?

  doc.puts("|#{header}")
  doc.puts('a|')
  items.each { |pf| doc.puts(yield(pf)) }
  doc.puts
end
write_package_list(doc, section) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 496
def write_package_list(doc, section)
  packages = section.components.select { |c| c.is_a? Package }
  sorted = packages.sort_by(&:name)
  doc.puts('.Package Versions:')
  sorted.each do |pkg|
    doc.puts("- #{pkg.name} #{pkg.version}")
  end
end
write_parameter(doc, params) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 283
def write_parameter(doc, params)
  params.each do |param|
    pname = param['name'].first
    pdefault = param['default'].first.gsub(/\\\n  /, '')
    default_string = if pdefault == '(empty)'
                       'not set'
                     else
                       "`#{pdefault}`"
                     end

    value = @parameters[pname].gsub(/\\\n  /, '')
    val_string = if value == ''
                   'not set'
                 else
                   "`#{value}`"
                 end

    doc.print("Parameter: #{pname}:: ")
    if val_string == default_string
      doc.puts("Value: #{val_string} _(default)_")
    else
      doc.puts("Value: #{val_string} _(default: #{default_string})_")
    end
  end
end
write_patch_block(doc, package, patches_to_render) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 463
def write_patch_block(doc, package, patches_to_render)
  doc.puts('.Patch:')
  patches_to_render.each do |patch|
    doc.puts("- #{package.name_and_version}-#{patch}.patch")
  end
end
write_servicedir(doc, svcdefs) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 229
def write_servicedir(doc, svcdefs)
  svcdefs.each do |svcdef|
    sd = ServiceDir.new(svcdef)
    head = ".Service Directory: #{sd.type.capitalize} `#{sd.name}`"
    head += " (in bundle `#{sd.bundle}`)" if sd.bundle
    doc.puts(head)
    doc.puts("[%autowidth,cols=\"d,d\",caption=]\n|===\n\n")
    write_servicedir_body(doc, sd)
    doc.puts('|===')
  end
end
write_servicedir_body(doc, svc) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 256
def write_servicedir_body(doc, svc)
  svc.oneline_files.keys.sort.each do |fn|
    next if fn == 'type'

    doc.puts("|#{fn}|`#{svc.oneline_files[fn]}`")
  end
  unless svc.dependencies.empty?
    doc.puts("|Dependencies\na|")
    svc.dependencies.each do |dep|
      doc.puts("- `#{dep}`")
    end
    doc.puts
  end
  unless svc.env.empty?
    doc.puts("|Environment\na|")
    svc.env.keys.sort.each do |var|
      doc.puts("`#{var}`:: `#{svc.env[var]}`")
    end
    doc.puts
  end
  svc.multiline_files.keys.sort.each do |filename|
    doc.puts("|#{filename.capitalize} script\nl|")
    doc.puts(svc.multiline_files[filename])
    doc.puts
  end
end
write_servicepipe(doc, pipedefs) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 241
def write_servicepipe(doc, pipedefs)
  pipedefs.each do |pipedef|
    head = ".Service Pipeline: `#{pipedef['name'].first}`"
    head += " (in bundle `#{pipedef['bundle'].first}`)" if pipedef['bundle']
    doc.puts(head)
    doc.puts("[%autowidth,cols=\"d,d\",caption=]\n|===\n\n")
    pipedef['servicedirs'].each_with_index do |svcdir, idx|
      sd = ServiceDir.new(svcdir)
      doc.puts("2+h|Service #{idx + 1}: #{sd.type.capitalize} `#{sd.name}`")
      write_servicedir_body(doc, sd)
    end
    doc.puts('|===')
  end
end
write_stage_commands(doc, cmds, stage_name) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 455
def write_stage_commands(doc, cmds, stage_name)
  doc.puts(".#{stage_name} commands:")
  doc.puts('[source,bash]')
  doc.puts('----')
  cmds.each { |cmd| doc.puts(cmd) }
  doc.puts('----')
end
write_toplevel_includes(location:, document:) click to toggle source

The include directives are a little bit complicated because in different cases we want to write them with different styles.

  • Usually we want to swallow the level-0 header in the included fragment, so that the document header becomes the level-0 header and the first section becomes a preamble. We do this by including the fragment with lines=“3..-1” so that the first two lines are ignored.

  • When we are writing a multi-part book, we do that for the first part but include the other fragments without any other arguments, so their level-0 header stays a level-0 header and becomes the part header.

  • When we are writing a single-part document (article, not book), and we have multiple fragments, that means that the requested target has dependencies and those dependencies are being written before the requested target. In that case, the document header is still the level-0 header, but we don't want to ignore the level-0 header in the included fragments – we increase leveloffset so they become level-1 headers, and so on.

  • For appendices, we always use a multi-part document style and include appendices as siblings to the parts. Since we want a level-0 header for the appendix itself but want other headers to start at level-3, we write a level-0 header into the top level document, swallow the level-0 header in the included fragment, and also increase the header level in the reset of the fragment so we can skip the level-2 headers.

# File lib/litbuild/ascii_doc_visitor.rb, line 530
def write_toplevel_includes(location:, document:)
  fragments = @written[location]
  if fragments.size > 1 && other_parts.empty?
    fragments.each do |fragment|
      document.puts
      document.puts("include::./#{fragment}[leveloffset=+1]")
    end
  else
    main_section = fragments.first
    document.puts
    document.puts("include::./#{main_section}[lines=\"3..-1\"]")
    other_parts.each do |part|
      partfile = fragments.detect { |x| x =~ /^[0-9]+-#{part}.adoc$/ }
      document.puts
      document.puts("include::./#{partfile}[]")
    end
    appendices.each do |appendix|
      appname = appendix.file_name
      appfile = fragments.detect { |x| x =~ /^[0-9]+-#{appname}.adoc$/ }
      document.puts("\n[appendix]")
      document.puts("[[#{appendix.name},#{appendix.header_text}]]")
      document.puts("= #{appendix.header_text}\n\n")
      document.puts("include::./#{appfile}[leveloffset=+1,lines=\"3..-1\"]")
    end
  end
end
write_url_row(doc, url, header) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 447
def write_url_row(doc, url, header)
  if url.match?(/\(.*\)/)
    doc.puts("|#{header}|_#{url}_")
  else
    doc.puts("|#{header}|#{url}")
  end
end
write_url_rows(doc, package) click to toggle source
# File lib/litbuild/ascii_doc_visitor.rb, line 441
def write_url_rows(doc, package)
  write_url_row(doc, package.url('project'), 'Project URL')
  write_url_row(doc, package.url('scm'), 'SCM URL')
  write_url_row(doc, package.url('download'), 'Download URL')
end