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
# 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
# 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
# File lib/litbuild/ascii_doc_visitor.rb, line 40 def visit_narrative(narrative:) write(blueprint: narrative, location: cwd) end
# 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
# 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
# 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
# File lib/litbuild/ascii_doc_visitor.rb, line 322 def dependency_anchor_list(dep_directives) anchors = dep_directives.map { |d| "<<#{d}>>" } anchors.join(', ') end
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
# 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
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
# 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
# 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
# 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
# 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 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
# 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
# File lib/litbuild/ascii_doc_visitor.rb, line 338 def write_build_dir(doc, build_dir) doc.puts("Build Directory:: `#{build_dir.first}`") end
# 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
# 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
# File lib/litbuild/ascii_doc_visitor.rb, line 327 def write_dependencies(doc, dep_directives) doc.puts("Dependencies:: #{dependency_anchor_list(dep_directives)}.") end
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
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
# 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
# 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