433 lines
9.7 KiB
Crystal
433 lines
9.7 KiB
Crystal
|
|
class SpecParser
|
|
|
|
class Exception < ::Exception
|
|
end
|
|
|
|
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.split(",").map(&.gsub /(^ *| *$)/, "")
|
|
end
|
|
|
|
def as_s_or_ls : String
|
|
@value
|
|
end
|
|
|
|
def initialize(@value)
|
|
end
|
|
end
|
|
|
|
class LongStringContainer
|
|
SpecParser.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 ArrayContainer
|
|
SpecParser.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
|
|
|
|
class Section
|
|
SpecParser.incompatible_methods as_s, as_a_or_s, as_s_or_ls
|
|
property name : String
|
|
property options : Array(String)
|
|
property content : Hash(String, StringContainer | ArrayContainer | LongStringContainer)
|
|
|
|
def initialize(@name)
|
|
@options = Array(String).new
|
|
@content = Hash(String, StringContainer | ArrayContainer | LongStringContainer).new
|
|
end
|
|
end
|
|
|
|
|
|
property assignments : Hash(String, StringContainer | LongStringContainer | ArrayContainer)
|
|
property sections : Array(Section)
|
|
property current_line_number = 0
|
|
|
|
def initialize
|
|
@assignments = Hash(String, StringContainer | LongStringContainer | ArrayContainer).new
|
|
@sections = Array(Section).new
|
|
end
|
|
|
|
def parse_assignment (line : String, content : Array(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
|
|
|
|
# In case the content is over several lines.
|
|
value = grab_content content, value
|
|
|
|
@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?
|
|
@current_line_number += 1
|
|
case line
|
|
when /^[ \t]*(#.*)?$/
|
|
# puts "blank line or comment, still in a list"
|
|
when /^[ \t]+-([^#]*)/
|
|
# puts "line content: #{$~[1]}"
|
|
current_line = $~[1].lstrip(" ")
|
|
list.push grab_content(content, current_line)
|
|
else
|
|
content.unshift line
|
|
@current_line_number -= 1
|
|
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?
|
|
@current_line_number += 1
|
|
case line
|
|
when /^[ \t]*(#.*)?$/
|
|
# puts "blank line or comment, still in a code block"
|
|
when /^[ \t]+(.*)/
|
|
# puts "code content: #{$~[1]}"
|
|
# old version: str << "#{$~[1].lstrip(" ").rstrip(" ")}"
|
|
# new version does not change the line (better shell integration)
|
|
str << "#{$~[1]}\n"
|
|
else
|
|
content.unshift line
|
|
@current_line_number -= 1
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if name.nil?
|
|
return
|
|
end
|
|
if value.nil?
|
|
return
|
|
end
|
|
|
|
@assignments[name] = LongStringContainer.new value
|
|
end
|
|
|
|
def parse_section(header : String, content : Array(String))
|
|
results = /^[%]([a-zA-Z][a-zA-Z0-9-_]*)[ \t]*([^#]*)/.match(header)
|
|
name = results[1]? if results
|
|
options = results[2]? if results
|
|
|
|
if name.nil?
|
|
return
|
|
end
|
|
|
|
section = Section.new name
|
|
unless options.nil?
|
|
section.options = options.split(",").map &.lstrip(" ").rstrip(" ")
|
|
end
|
|
|
|
while line = content.shift?
|
|
@current_line_number += 1
|
|
case line
|
|
when /^[ \t]*#.*$/
|
|
# puts "blank line or comment, still in a section"
|
|
when /^([ \t]+)([^:]+):[ \t]+(.+)/
|
|
aname = $~[2].lstrip(" ").rstrip(" ")
|
|
avalue = $~[3].lstrip(" ").rstrip(" ")
|
|
# puts "simple assignation: #{aname} => #{avalue}"
|
|
section.content[aname] = StringContainer.new avalue
|
|
|
|
when /^([ \t]+)[@]([^ \t#]+)/ # free text
|
|
indent = $~[1]
|
|
codeblockname = $~[2].lstrip(" ").rstrip(" ")
|
|
# puts "code block name: #{codeblockname}"
|
|
|
|
codeblockvalue = String.build do |str|
|
|
while line = content.shift?
|
|
@current_line_number += 1
|
|
case line
|
|
when /^[ \t]*#.*?$/
|
|
# puts "blank line or comment, still in a free text in a section"
|
|
when /#{indent}[ \t]+(.*)/
|
|
# puts "code content in section: #{$~[1]}"
|
|
str << "#{$~[1]}\n"
|
|
else
|
|
content.unshift line
|
|
@current_line_number -= 1
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
section.content[codeblockname] = LongStringContainer.new codeblockvalue
|
|
|
|
when /^([ \t]+)([^:]+):/
|
|
indent = $~[1]
|
|
lname = $~[2].lstrip(" ").rstrip(" ")
|
|
# puts "list: #{lname}"
|
|
|
|
list = Array(String).new
|
|
while line = content.shift?
|
|
@current_line_number += 1
|
|
case line
|
|
when /^[ \t]*#.*?$/
|
|
# puts "blank line or comment, still in a list in a section"
|
|
when /^#{indent}[ \t]+-([^#]*)/
|
|
lcontent = $~[1].lstrip(" ").rstrip(" ")
|
|
# puts "list item in section: #{lcontent}"
|
|
list.push lcontent
|
|
else
|
|
content.unshift line
|
|
@current_line_number -= 1
|
|
break
|
|
end
|
|
end
|
|
|
|
if list.empty?
|
|
next
|
|
end
|
|
|
|
section.content[lname] = ArrayContainer.new list
|
|
else
|
|
content.unshift line
|
|
@current_line_number -= 1
|
|
break
|
|
end
|
|
end
|
|
|
|
sections.push section
|
|
end
|
|
|
|
# Remove \n when line ends with a backslash.
|
|
def grab_content(content : Array(String), current_value : String)
|
|
while current_value =~ /\\$/
|
|
# Remove trailing backslash
|
|
current_value = current_value.rstrip "\\"
|
|
nl = content.shift?
|
|
if nl
|
|
@current_line_number += 1
|
|
current_value += nl.lstrip
|
|
else
|
|
raise Exception.new "last line is ending with a backslash"
|
|
end
|
|
end
|
|
|
|
current_value
|
|
end
|
|
|
|
def parse_lines(content : Array(String))
|
|
while line = content.shift?
|
|
@current_line_number += 1
|
|
|
|
case line
|
|
when /^[a-zA-Z][a-zA-Z0-9-_]*:[ \t]*([#].*)?$/
|
|
parse_list line, content
|
|
when /^[a-zA-Z][a-zA-Z0-9-_]*:[ \t]+[^#]+/
|
|
parse_assignment line, content
|
|
when /^[@][a-zA-Z][a-zA-Z0-9-_]*[ \t]*([#].*)?/
|
|
parse_code_block line, content
|
|
when /^[%][a-zA-Z][a-zA-Z0-9-_]*[ \t]*([#].*)?/
|
|
parse_section line, content
|
|
when /^[ \t]*#/
|
|
# puts "comment"
|
|
when /^[ \t]+$/
|
|
# puts "empty line"
|
|
when /^[ \t]+[^ \t#]/
|
|
raise Exception.new "line #{@current_line_number}: line starting with spaces or a tabulation outside a list or a section: should not happen"
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
|
|
def replace_string (str : String)
|
|
reg = /%\{([^}]*)\}/
|
|
|
|
while str =~ reg
|
|
x = reg.match(str)
|
|
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
|
|
str = str.gsub "%{#{var[0]}}", "#{replacement_value}"
|
|
end
|
|
else
|
|
# TODO: search in sections for assignments?
|
|
raise "cannot find variable \033[31m#{var[0]}\033[00m"
|
|
end
|
|
end
|
|
end
|
|
|
|
str
|
|
end
|
|
|
|
def replace_string_obj (v : StringContainer | LongStringContainer)
|
|
is_missing_references = false
|
|
value = v.value
|
|
|
|
begin
|
|
v.value = replace_string value
|
|
rescue e
|
|
puts "#{e}"
|
|
is_missing_references = true
|
|
end
|
|
|
|
if is_missing_references
|
|
raise "there are missing references in the document: fix it"
|
|
end
|
|
end
|
|
|
|
def replace_array (a : Array(String))
|
|
newarray = Array(String).new
|
|
|
|
is_missing_references = false
|
|
|
|
a.each do |value|
|
|
begin
|
|
newarray.push replace_string(value)
|
|
rescue e
|
|
puts "#{e}"
|
|
is_missing_references = true
|
|
end
|
|
end
|
|
|
|
if is_missing_references
|
|
raise "there are missing references in the document: fix it"
|
|
end
|
|
|
|
newarray
|
|
end
|
|
|
|
def replace_array_obj (v : ArrayContainer)
|
|
str_array = v.value
|
|
newarray = replace_array str_array
|
|
v.value = newarray
|
|
end
|
|
|
|
def replace_section_obj (v : Section)
|
|
is_missing_references = false
|
|
v.options = replace_array v.options
|
|
v.content.map do |k, v|
|
|
case v
|
|
when StringContainer
|
|
replace_string_obj v
|
|
when LongStringContainer
|
|
replace_string_obj v
|
|
when ArrayContainer
|
|
replace_array_obj v
|
|
end
|
|
end
|
|
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 LongStringContainer
|
|
replace_string_obj v
|
|
when ArrayContainer
|
|
replace_array_obj v
|
|
end
|
|
end
|
|
|
|
sections.each do |section|
|
|
replace_section_obj section
|
|
end
|
|
end
|
|
|
|
|
|
#
|
|
# public functions
|
|
#
|
|
|
|
def self.parse_file(file_name : String, options : Hash(String, String) | Nil = nil) : SpecParser
|
|
content = File.read(file_name)
|
|
content = content.rchop
|
|
|
|
specs = parse content, options
|
|
end
|
|
|
|
def self.parse(data : String, options : Hash(String, String) | Nil = nil) : SpecParser
|
|
specs = SpecParser.new
|
|
unless options.nil?
|
|
options.each do |opt, val|
|
|
specs.assignments[opt] = StringContainer.new val
|
|
end
|
|
end
|
|
|
|
specs.parse_lines (data.split("\n"))
|
|
|
|
specs.rewrite
|
|
|
|
specs
|
|
rescue e
|
|
raise Exception.new "unexpected parser error: #{e}, current line #{specs.not_nil!.current_line_number}"
|
|
end
|
|
end
|