This repository has been archived on 2022-01-17. You can view files and clone it, but cannot push or open issues/pull-requests.
recipes-parser/src/spec.cr

405 lines
8.6 KiB
Crystal

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 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
class Section
Specs.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 | Array(Section) | ArrayContainer)
def initialize
@assignments = Hash(String, StringContainer | LongStringContainer | Array(Section) | ArrayContainer).new
@assignments["sections"] = Array(Section).new
end
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_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?
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?
case line
when /^[ \t]*#.*?$/
# puts "blank line or comment, still in a free text in a section"
when /#{indent}[ \t]+(.*)/
v = $~[1].lstrip(" ").rstrip(" ")
# puts "freetext content in section: #{v}"
# puts "code content: #{$~[1]}"
str << "#{v};"
else
content.unshift line
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?
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
break
end
end
if list.empty?
next
end
section.content[lname] = ArrayContainer.new list
else
content.unshift line
break
end
end
sections = @assignments["sections"]
if sections.is_a?(Array(Section))
sections.push section
end
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-Z0-9]*:[ \t]*([#].*)?$/
parse_list line, content
when /^[a-zA-Z][a-zA-Z]*:[ \t]+[^#]+/
parse_assignment line
when /^[@][a-zA-Z][a-zA-Z]*[ \t]*([#].*)?/
parse_code_block line, content
when /^[%][a-zA-Z][a-zA-Z]*[ \t]*([#].*)?/
parse_section line, content
when /^[ \t]*#/
# puts "comment"
when /^[ \t]+/
puts "tab!! should not happen"
end
count += 1
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
when Array(Section)
v.each do |section|
replace_section_obj section
end
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