class Litbuild::BlueprintParser

This is a kludgy hand-built parser. Blueprint structure is not (at least, at this point) complicated enough that I can see any point in defining a grammar and using a parser generator. The structure of blueprint directives is described informally in `doc/blueprints.txt` (and blueprint-type-specific files under `doc` as well).

tl;dr: each paragraph can be either directives or narrative. Narrative is AsciiDoc and does not get parsed or transformed. Directives are basically a simplified version of YAML.

Attributes

base_directives[R]

directives are for use in scripts. grafs are for use in documents.

base_grafs[R]

directives are for use in scripts. grafs are for use in documents.

phase_directives[R]

directives are for use in scripts. grafs are for use in documents.

phase_grafs[R]

directives are for use in scripts. grafs are for use in documents.

Public Class Methods

new(file_text) click to toggle source
# File lib/litbuild/blueprint_parser.rb, line 22
def initialize(file_text)
  @text = file_text
  @phase_directives = {}
  @phase_grafs = {}
  phase_chunks = @text.split(/^phase: ([a-z -_]+)$/)

  # The first part of the blueprint, before any phase directive,
  # becomes the base directives and base narrative for the
  # blueprint. If there are no phase directives, this is the entire
  # blueprint, obvs.
  @base_grafs = []
  base = parse_phase(phase_chunks.shift, {}, @base_grafs)
  base['full-name'] ||= base['name']
  @base_directives = base

  # The rest of the blueprint, if any, consists of directives and
  # narrative for specific phases. The directives for each phase
  # include all the base directives, as well as those specific to
  # the phase.
  until phase_chunks.empty?
    phase_name = phase_chunks.shift
    phase_contents = phase_chunks.shift
    grafs = []
    @phase_directives[phase_name] = parse_phase(phase_contents, base, grafs)
    @phase_grafs[phase_name] = grafs
  end

  # Any directives at the beginning of a blueprint are actually a
  # file header that should not be rendered as part of the narrative
  # for it.
  @base_grafs.shift while @base_grafs[0].is_a?(Hash)
rescue StandardError => e
  msg = "Cannot parse blueprint starting: #{@text.lines[0..3].join}"
  raise(Litbuild::ParseError, "#{msg} -- #{e}")
end

Private Instance Methods

add_directives(directives, parsed) click to toggle source
# File lib/litbuild/blueprint_parser.rb, line 103
def add_directives(directives, parsed)
  directives.merge!(parsed) do |_key, old_val, new_val|
    [old_val, new_val].flatten
  end
end
array_value(lines) click to toggle source
# File lib/litbuild/blueprint_parser.rb, line 184
def array_value(lines)
  value = []
  until lines.empty?
    array_member = lines.shift
    firstline_value = /^- *(.*)$/.match(array_member)[1]
    related = related_lines(array_member, lines)
    value << parse_directive_value(firstline_value, related)
  end
  value
rescue StandardError
  raise(Litbuild::ParseError, "Problem parsing: #{lines}")
end
directives?(paragraph) click to toggle source
# File lib/litbuild/blueprint_parser.rb, line 99
def directives?(paragraph)
  paragraph.split(' ').first =~ /^[a-z-]+:/
end
folded_continuation_lines(first_line, related) click to toggle source

any time a directive line is indented more than the previous line, without being a part of an array or sub-directive or multi-line directive, it's a continuation of the previous line.

# File lib/litbuild/blueprint_parser.rb, line 174
def folded_continuation_lines(first_line, related)
  stripped = related.map(&:strip)
  stripped.unshift(first_line)
  stripped.join(" \\\n  ")
end
handle_servicedirs(directives) click to toggle source

All s6-rc service directories should be tracked in the configuration file repository, so add them to `configuration-files`.

# File lib/litbuild/blueprint_parser.rb, line 82
def handle_servicedirs(directives)
  cfgs = directives['configuration-files'] || []
  if (spipes = directives['service-pipeline'])
    spipes.each do |a_pipe|
      a_pipe['servicedirs'].each do |a_svc|
        cfgs << "/etc/s6-rc/source/#{a_svc['name'].first}"
      end
    end
  end
  if (sdirs = directives['servicedir'])
    sdirs.each do |a_svc|
      cfgs << "/etc/s6-rc/source/#{a_svc['name'].first}"
    end
  end
  directives['configuration-files'] = cfgs unless cfgs.empty?
end
indent_for(line) click to toggle source

Utility method to find the amount of blank space at the beginning of a line.

# File lib/litbuild/blueprint_parser.rb, line 206
def indent_for(line)
  /^([[:blank:]]*).*/.match(line)[1].size
end
multiline_value(lines) click to toggle source
# File lib/litbuild/blueprint_parser.rb, line 180
def multiline_value(lines)
  lines.join("\n") + "\n"
end
parse_directive_value(firstline_value, other_lines) click to toggle source

What kind of directive are we dealing with? Could be a simple value (with zero or more continuation lines), or a multiline value, or an array, or a subdirective block.

# File lib/litbuild/blueprint_parser.rb, line 157
def parse_directive_value(firstline_value, other_lines)
  if firstline_value == '|'
    multiline_value(other_lines)
  elsif other_lines.empty?
    firstline_value
  elsif other_lines[0].match?(/^ *- /)
    array_value(other_lines)
  elsif other_lines[0].match?(/^[A-Za-z_-]+: /)
    subdirective_value(other_lines)
  else
    folded_continuation_lines(firstline_value, other_lines)
  end
end
parse_lines(lines_to_process) click to toggle source
# File lib/litbuild/blueprint_parser.rb, line 114
def parse_lines(lines_to_process)
  graf_directives = Hash.new { |h, k| h[k] = [] }
  until lines_to_process.empty?
    directive_line = lines_to_process.shift
    md = /^([A-Za-z0-9_-]+): *(.*)/.match(directive_line)
    unless md
      raise(Litbuild::ParseError,
            "Expected '#{directive_line}' to be a directive")
    end
    directive_name = md[1]
    firstline_value = md[2]
    value_lines = related_lines(directive_line, lines_to_process)
    value = parse_directive_value(firstline_value, value_lines)
    if value == "''"
      # two literal apostrophes is a special case, we treat it as an
      # empty string. Probably ought to document that.
      graf_directives[directive_name] << ''
    else
      graf_directives[directive_name] << value
    end
    graf_directives[directive_name].flatten!
  end
  graf_directives
end
parse_paragraph(paragraph) click to toggle source
# File lib/litbuild/blueprint_parser.rb, line 109
def parse_paragraph(paragraph)
  lines_to_process = paragraph.lines.map(&:rstrip)
  parse_lines(lines_to_process)
end
parse_phase(blueprint_text, start_with_directives, graf_collector) click to toggle source

Parse all directive paragraphs found in blueprint_text. Also add all parsed directive paragraphs, and all narrative paragraphs, to a collecting parameter.

# File lib/litbuild/blueprint_parser.rb, line 63
def parse_phase(blueprint_text, start_with_directives, graf_collector)
  directives = JSON.parse(JSON.generate(start_with_directives))
  paragraphs = blueprint_text.split(/\n\n+/m)
  paragraphs.each do |paragraph|
    if directives?(paragraph)
      parsed = parse_paragraph(paragraph)
      add_directives(directives, parsed)
      graf_collector << parsed
    else
      graf_collector << paragraph
    end
  end
  handle_servicedirs(directives)
  directives
end
subdirective_value(lines) click to toggle source

It turns out that sub-directives are the easiest thing in the world to parse, since we can just take the value and parse it as a new directive block.

# File lib/litbuild/blueprint_parser.rb, line 200
def subdirective_value(lines)
  parse_lines(lines)
end