class Specs macro incompatible_methods(*names) {% for name in names %} {% if name.id == "as_s" %} def {{name}} : String raise "short string expected; which does not exist in #{self.class}" end {% elsif name.id == "as_a_or_s" %} def {{name}} : Array(String) raise "list or string expected; which does not exist in #{self.class}" end {% elsif name.id == "as_s_or_ls" %} def {{name}} : String raise "string or multiline text section expected; which does not exist in #{self.class}" end {% end %} {% end %} end class StringContainer property value : String def as_s : String @value end def as_a_or_s : Array(String) # FIXME: We should probably be splitting the string around comas. [@value] end def as_s_or_ls : String @value end def initialize(@value) end end class LongStringContainer Specs.incompatible_methods as_s, as_a_or_s property value : String def as_s_or_ls : String @value end def initialize(@value) end end class SectionContainer Specs.incompatible_methods as_s, as_a_or_s, as_s_or_ls property value : String def initialize(@value) end end class ArrayContainer Specs.incompatible_methods as_s, as_s_or_ls property value : Array(String) def as_a_or_s : Array(String) @value end def initialize(@value) end end property assignments : Hash(String, StringContainer | LongStringContainer | SectionContainer | ArrayContainer) def initialize @assignments = Hash(String, StringContainer | LongStringContainer | SectionContainer | ArrayContainer).new end # current_line = current_line.lstrip(" ").rstrip(" ") def parse_assignment (line : String) # puts "simple assignment: #{line}" name = /([a-zA-Z][a-zA-Z0-9]*):/.match(line).try &.[1] value = /[a-zA-Z][a-zA-Z0-9]*: ([^#]*)/.match(line).try &.[1] if name.nil? return end if value.nil? return end @assignments[name] = StringContainer.new value.lstrip(" ").rstrip(" ") end def parse_list(header : String, content : Array(String)) name = /[a-zA-Z][a-zA-Z0-9]*/.match(header).try &.[0] # puts "new list: #{name}" list = Array(String).new while line = content.shift? case line when /^[ \t]*(#.*)?$/ # puts "blank line or comment, still in a list" when /^[ \t]+-([^#]*)/ # puts "line content: #{$~[1]}" list.push $~[1].lstrip(" ").rstrip(" ") else content.unshift line break end end if name.nil? return end if list.nil? return end @assignments[name] = ArrayContainer.new list end def parse_code_block(header : String, content : Array(String)) name = /[a-zA-Z][a-zA-Z0-9]*/.match(header).try &.[0] # puts "new code block: #{name}" value = String.build do |str| while line = content.shift? case line when /^[ \t]*(#.*)?$/ # puts "blank line or comment, still in a code block" when /^[ \t]+(.*)/ # puts "code content: #{$~[1]}" str << "#{$~[1].lstrip(" ").rstrip(" ")};" else content.unshift line break end end end if name.nil? return end if value.nil? return end @assignments[name] = LongStringContainer.new value end def parse_lines(content : Array(String)) count = 0 while line = content.shift? case line when /^[ \t]*$/ else # puts "line #{count}: #{line}" end case line when /^[a-zA-Z][a-zA-Z]*:[ \t]+[a-zA-Z0-9-;|'"]+/ parse_assignment line when /^[a-zA-Z][a-zA-Z]*:[ \t]*([#].*)?/ parse_list line, content when /^[@][a-zA-Z][a-zA-Z]*[ \t]*([#].*)?/ parse_code_block line, content when /^[ \t]*#/ # puts "comment" when /^[ \t]+/ # puts "tab!!" end count += 1 end end def replace_string_obj (v : StringContainer | SectionContainer | LongStringContainer) reg = /%\{([^}]*)\}/ is_missing_references = false value = v.value begin while value =~ reg x = reg.match(value) unless x.nil? var = x.captures() if var[0]? && @assignments[var[0]]? replacement_object = @assignments[var[0]] case replacement_object when StringContainer replacement_value = replacement_object.value value = value.gsub "%{#{var[0]}}", "#{replacement_value}" end else is_missing_references = true raise "cannot find variable \033[31m#{var[0]}\033[00m" end end v.value = value value = v.value end rescue e puts "#{e}" end if is_missing_references raise "there are missing references in the document: fix it" end end def replace_array_obj (v : ArrayContainer) reg = /%\{([^}]*)\}/ str_array = v.value newarray = Array(String).new is_missing_references = false str_array.each do |value| tmp = value begin while tmp =~ reg x = reg.match(tmp) unless x.nil? var = x.captures() if var[0]? && @assignments[var[0]]? replacement_object = @assignments[var[0]] case replacement_object when StringContainer replacement_value = replacement_object.value tmp = tmp.gsub "%{#{var[0]}}", "#{replacement_value}" end else is_missing_references = true raise "cannot find variable \033[31m#{var[0]}\033[00m" end end end rescue e puts "#{e}" end newarray.push tmp end if is_missing_references raise "there are missing references in the document: fix it" end v.value = newarray end def rewrite # replaces all occurrences of %{variable} by the content of @assignments[variable] @assignments.map do |k, v| case v when StringContainer replace_string_obj v when SectionContainer replace_string_obj v when LongStringContainer replace_string_obj v when ArrayContainer replace_array_obj v end end end # The only function to use from outside. def self.parse(file_name : String, options : Hash(String, String) | Nil = nil) : Specs | Nil begin content = File.read(file_name) content = content.rchop specs = Specs.new unless options.nil? options.each do |opt, val| specs.assignments[opt] = StringContainer.new val end end specs.parse_lines (content.split("\n")) specs.rewrite specs rescue e puts "Exception: #{e}" nil end end end