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
directives are for use in scripts. grafs are for use in documents.
directives are for use in scripts. grafs are for use in documents.
directives are for use in scripts. grafs are for use in documents.
directives are for use in scripts. grafs are for use in documents.
Public Class Methods
# 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
# 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
# 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
# File lib/litbuild/blueprint_parser.rb, line 99 def directives?(paragraph) paragraph.split(' ').first =~ /^[a-z-]+:/ end
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
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
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
# File lib/litbuild/blueprint_parser.rb, line 180 def multiline_value(lines) lines.join("\n") + "\n" end
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
# 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
# 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 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
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