fixture_fox 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +7 -0
  7. data/README.md +36 -0
  8. data/Rakefile +6 -0
  9. data/TODO +79 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/doc/diagram.drawio +1 -0
  13. data/examples/1-1-subrecord.fox +15 -0
  14. data/examples/1-1-subrecord.sql +22 -0
  15. data/examples/N-M.fox +25 -0
  16. data/examples/N-M.sql +34 -0
  17. data/examples/anchors.sql +13 -0
  18. data/examples/anchors.yml +4 -0
  19. data/examples/anchors1.fox +4 -0
  20. data/examples/anchors2.fox +6 -0
  21. data/examples/array.fox +84 -0
  22. data/examples/array.sql +53 -0
  23. data/examples/base.fox +81 -0
  24. data/examples/base.sql +52 -0
  25. data/examples/empty.fox +8 -0
  26. data/examples/empty.sql +33 -0
  27. data/examples/include/schema-included.fox +23 -0
  28. data/examples/inherit.fox +26 -0
  29. data/examples/inherit.sql +28 -0
  30. data/examples/kind.fox +17 -0
  31. data/examples/kind.sql +31 -0
  32. data/examples/link.fox +35 -0
  33. data/examples/link.sql +32 -0
  34. data/examples/root.fox +22 -0
  35. data/examples/schema-fragment-1.fox +9 -0
  36. data/examples/schema-fragment-2.fox +21 -0
  37. data/examples/schema-include.fox +10 -0
  38. data/examples/schema-indent.fox +29 -0
  39. data/examples/schema.fox +29 -0
  40. data/examples/schema.sql +33 -0
  41. data/examples/types.fox +8 -0
  42. data/examples/types.sql +17 -0
  43. data/examples/views.fox +15 -0
  44. data/examples/views.sql +38 -0
  45. data/exe/fox +178 -0
  46. data/fixture_fox.gemspec +37 -0
  47. data/lib/fixture_fox/analyzer.rb +371 -0
  48. data/lib/fixture_fox/anchor.rb +93 -0
  49. data/lib/fixture_fox/ast.rb +176 -0
  50. data/lib/fixture_fox/error.rb +23 -0
  51. data/lib/fixture_fox/hash_parser.rb +111 -0
  52. data/lib/fixture_fox/idr.rb +62 -0
  53. data/lib/fixture_fox/line.rb +217 -0
  54. data/lib/fixture_fox/parser.rb +153 -0
  55. data/lib/fixture_fox/token.rb +173 -0
  56. data/lib/fixture_fox/tokenizer.rb +78 -0
  57. data/lib/fixture_fox/version.rb +3 -0
  58. data/lib/fixture_fox.rb +216 -0
  59. metadata +227 -0
@@ -0,0 +1,111 @@
1
+ class HashParser
2
+ def self.parse(s)
3
+ self.new(s).send(:parse)
4
+ end
5
+
6
+ private
7
+ def initialize(s)
8
+ @s, @i = s, 0
9
+ end
10
+
11
+ def parse
12
+ @s[@i] == "{" or raise "Missing initial '{' in hash literal: #{@s.inspect}"
13
+ @i += 1
14
+ pairs = []
15
+ while @i < @s.size && @s[@i] !~ /[,\}]/
16
+ pairs << extract_key_value
17
+ extract_element_separator
18
+ end
19
+ @s[@i] == "}" or raise "Missing terminating '}' in hash literal: #{@s.inspect}"
20
+ Hash[*pairs.flatten]
21
+ end
22
+
23
+ def extract_element_separator
24
+ while @i < @s.size && @s[@i] =~ /\s/
25
+ @i += 1
26
+ end
27
+ if @i < @s.size && @s[@i] == ","
28
+ @i += 1
29
+ while @i < @s.size && @s[@i] =~ /\s/
30
+ @i += 1
31
+ end
32
+ end
33
+ end
34
+
35
+ def extract_key_value
36
+ key = extract_key
37
+ extract_separator
38
+ value = extract_value
39
+ [key.to_sym, value]
40
+ end
41
+
42
+ def extract_key
43
+ key = ""
44
+ while @i < @s.size && @s[@i] =~ /\w/
45
+ key += @s[@i]
46
+ @i += 1
47
+ end
48
+ key
49
+ end
50
+
51
+ def extract_separator
52
+ while @i < @s.size && @s[@i] =~ /[\s:]/
53
+ @i += 1
54
+ end
55
+ end
56
+
57
+ def extract_value
58
+ if @s[@i] =~ /['"]/
59
+ extract_quoted_value
60
+ else
61
+ extract_unquoted_value
62
+ end
63
+ end
64
+
65
+ def extract_quoted_value
66
+ quot = @s[@i]
67
+ @i += 1
68
+
69
+ litt = ""
70
+ while @i < @s.size && @s[@i] != quot
71
+ if @s[@i] == "\\"
72
+ @i += 1
73
+ @i < @s.size or raise "Syntax error in hash literal: #{@s.inspect}"
74
+ case @s[@i]
75
+ when "\\"; litt += "\\"
76
+ when quot; litt += quot
77
+ else
78
+ litt += "\\" + @s[@i]
79
+ end
80
+ else
81
+ litt += @s[@i]
82
+ end
83
+ @i += 1
84
+ end
85
+
86
+ @i < @s.size && @s[@i] == quot or raise "Unterminated hash literal: #{@s.inspect}"
87
+ @i += 1
88
+ return litt
89
+ end
90
+
91
+ def extract_unquoted_value
92
+ j = @i + 1
93
+ while j < @s.size && @s[j] !~ /[,\}]/
94
+ j += 1
95
+ end
96
+ litt = @s[@i...j].strip
97
+ @i = j
98
+ value =
99
+ case litt
100
+ when "nil"; nil
101
+ when "true"; true
102
+ when "false"; false
103
+ when /^\d+$/; litt.to_i
104
+ else
105
+ litt.strip
106
+ end
107
+ value
108
+ end
109
+ end
110
+
111
+
@@ -0,0 +1,62 @@
1
+
2
+ require "tempfile"
3
+
4
+ module FixtureFox
5
+ class Idr
6
+ # Qualified names of controlled tables
7
+ def tables() @tables_hash.keys end
8
+
9
+ # List of materialized views that depends on the tables. Assigned by the analyzer
10
+ # FIXME: Is this in use?
11
+ attr_accessor :materialized_views
12
+
13
+ # Data as a hash from schema to table to id to record to field to value.
14
+ # Ie. { "schema" => { "table" => { 1 => { id: 1, name: "Alice" } } } }
15
+ attr_reader :data
16
+
17
+ # Map from qualified table name to last used ID. FIXME: Unused?
18
+ attr_reader :ids
19
+
20
+ def initialize
21
+ @tables_hash = {}
22
+ @data = {}
23
+ end
24
+
25
+ # Number of records
26
+ def count()
27
+ count = 0
28
+ @data.each { |_, tables| tables.each { |_, records| count += records.size } }
29
+ count
30
+ end
31
+
32
+ # Add a field to the data representation. If field and value are nil, an
33
+ # empty record will be created. If id is also nil, an empty table is
34
+ # created
35
+ #
36
+ def put(schema, table, id = nil, field = nil, value = nil)
37
+ # puts "Idr.put(#{schema}, #{table}, #{id.inspect}, #{field.inspect}, #{value.inspect})"
38
+ !id.nil? || field.nil? or raise ArgumentError
39
+ raise if table.to_s =~ /[A-Z]/
40
+ uid = "#{schema}.#{table}"
41
+ @tables_hash[uid] = true
42
+ return if id.nil?
43
+ tuple = ((@data[schema] ||= {})[table] ||= {})[id] ||= {id: id}
44
+ tuple[field.to_sym] = value if field
45
+ end
46
+
47
+ alias_method :to_h, :data
48
+
49
+ def dump
50
+ data.sort_by(&:first).each { |schema, tables|
51
+ puts schema
52
+ tables.each { |table, records|
53
+ puts " #{table}"
54
+ records.each { |id, fields|
55
+ puts " #{fields.inspect}"
56
+ }
57
+ }
58
+ }
59
+ end
60
+ end
61
+ end
62
+
@@ -0,0 +1,217 @@
1
+ module FixtureFox
2
+ class TokenizedLine
3
+ attr_reader :file
4
+ attr_reader :line
5
+ attr_reader :lineno
6
+ attr_reader :initial_indent
7
+ attr_reader :indent
8
+
9
+ def initialize(file, lineno, initial_indent, indent, line)
10
+ @file, @line, @lineno, @initial_indent, @indent = file, line.dup, lineno, initial_indent, indent
11
+ @pos = 1 + @indent # Used to keep track of position for the make_* methods
12
+ end
13
+
14
+ def error(msg)
15
+ raise ParseError.new(@file, @lineno, @pos + initial_indent, msg)
16
+ end
17
+
18
+ def to_s(long: false)
19
+ if long
20
+ "#{file}:#{lineno} "
21
+ else
22
+ "line #{lineno} "
23
+ end
24
+ end
25
+
26
+ protected
27
+ def make_token(litt)
28
+ Token.new(file, lineno, initial_indent, @pos, litt)
29
+ end
30
+
31
+ def make_ident(litt)
32
+ Ident.new(file, lineno, initial_indent, @pos, litt)
33
+ end
34
+
35
+ def make_empty(litt)
36
+ Empty.new(file, lineno, initial_indent, @pos, litt)
37
+ end
38
+
39
+ def make_value(litt)
40
+ Value.new(file, lineno, initial_indent, @pos, litt)
41
+ end
42
+
43
+ def make_directive(litt)
44
+ Directive.new(file, lineno, initial_indent, @pos, litt)
45
+ end
46
+
47
+ def make_anchor(litt)
48
+ AnchorToken.new(@file, @lineno, initial_indent, @pos, litt)
49
+ end
50
+
51
+ def make_reference(litt)
52
+ Reference.new(@file, @lineno, initial_indent, @pos, litt)
53
+ end
54
+ end
55
+
56
+ class DirectiveLine < TokenizedLine
57
+ attr_reader :directive # Directive token
58
+ attr_reader :argument # Value token
59
+
60
+ def initialize(file, lineno, initial_indent, indent, line)
61
+ super
62
+ case line
63
+ when /^(@schema)(\s+)(\w+)\s*(?:#.*)?$/
64
+ @directive = make_directive($1)
65
+ @pos += $1.size + $2.size
66
+ @argument = make_value($3)
67
+ when /^(@include)(\s+)(\S+)\s*(?:#.*)?$/
68
+ @directive = make_directive($1)
69
+ @pos += $1.size + $2.size
70
+ @argument = make_value($3)
71
+ when /^(@anchors)(\s+)(\S+)\s*(?:#.*)?$/
72
+ @directive = make_directive($1)
73
+ @pos += $1.size + $2.size
74
+ @argument = make_value($3)
75
+ else
76
+ error("Illegal directive")
77
+ end
78
+ end
79
+
80
+ def to_s(long: false)
81
+ super +
82
+ if long
83
+ "directive:#{directive.pos} #{directive.litt}, argument:#{argument.pos} #{argument.litt}"
84
+ else
85
+ "#{directive.litt} #{argument.litt}"
86
+ end
87
+ end
88
+ end
89
+
90
+ class Line < TokenizedLine
91
+ attr_reader :dash
92
+ attr_reader :ident
93
+ attr_reader :empty # Empty token
94
+ attr_reader :value
95
+ attr_reader :reference
96
+ attr_reader :anchor
97
+
98
+ attr_reader :directive # SchemaDirective token
99
+
100
+ def initialize(file, lineno, initial_indent, indent, line)
101
+ super
102
+
103
+ # Remove comments not preceded by a quote (' or ")
104
+ if line =~ /^([^'"#]*?)\s*#.*/
105
+ line = $1
106
+ end
107
+
108
+ # Parse root element
109
+ if indent == 0 && line[0] != "-"
110
+ # Table
111
+ case line
112
+ when /^(\w+(?:\.\w+)?)$/
113
+ @ident = make_ident($1)
114
+ return
115
+ when /^(\w+(?:\.\w+)?)(\s*:\s*)(\[\])$/
116
+ @ident = make_ident($1)
117
+ @pos += $1.size + $2.size
118
+ @empty = make_empty($3)
119
+ return
120
+ end
121
+
122
+ # Check for missing value in root record
123
+ if line =~ /^(\w+\s*:)$/
124
+ error "Table name expected"
125
+ end
126
+ end
127
+
128
+ # Parse dash
129
+ case line
130
+ when /^-$/
131
+ error "Entry content expected"
132
+ when /^(-\s+)(&\w+.*)/
133
+ @dash = make_token("-")
134
+ @pos += $1.size
135
+ @anchor = make_anchor($2.sub(/#.*/, ""))
136
+ return
137
+ when /^(-\s+)(.*)/
138
+ @dash = make_token("-")
139
+ @pos += $1.size
140
+ line = $2
141
+ when /^-./
142
+ @pos += 1
143
+ error "Illegal character after '-'"
144
+ end
145
+
146
+ # Expect key/value pair
147
+ case line
148
+ when /^(\w+)(\s*):\s*$/ # Record
149
+ @ident = make_ident($1)
150
+ @pos += $1.size + $2.size + 1
151
+ when /^(\w+)(\s*:\s*)(&\w+)(\s*)(\S.*)?$/ # Anchor
152
+ @ident = make_ident($1)
153
+ @pos += $1.size + $2.size
154
+ @anchor = make_anchor($3)
155
+ @pos += $3.size + $4.size
156
+ $5.nil? or error "Illegal character after anchor"
157
+ when /^(\w+)(\s*:\s*)(\*.+)(\s*)(\S.*)?/ # Reference
158
+ @ident = make_ident($1)
159
+ @pos += $1.size + $2.size
160
+ @reference = make_reference($3)
161
+ @pos += $3.size + $4.size
162
+ $5.nil? or error "Illegal character after reference"
163
+ when /^(\w+)(\s*:\s*)(["']?.+)/ # Simple value
164
+ @ident = make_ident($1)
165
+ @pos += $1.size + $2.size
166
+ @value = make_value($3)
167
+ when /^(\w+)(\s*:\s*)(\[.*)/ # Array value
168
+ @ident = make_ident($1)
169
+ @pos += $1.size + $2.size
170
+ litt =~ /.*\]\s*$/ or error "Missing ']'"
171
+ @value = make_value($3)
172
+ when /^(\w+)(\s*:\s*)(\{.*)/ # Hash value
173
+ @ident = make_ident($1)
174
+ @pos += $1.size + $2.size
175
+ litt =~ /.*\]\s*$/ or error "Missing '}'"
176
+ @value = make_value($3)
177
+ when /^(\*.+)(\s*)(\S.*)?$/
178
+ @reference = make_reference($1)
179
+ @pos += $1.size + $2.size
180
+ $3.nil? or error "Illegal character after reference"
181
+ when /^\[.*\]\s*$/
182
+ else
183
+ @pos += line.size
184
+ error "Missing value"
185
+ end
186
+ end
187
+
188
+ # True if the line is the first line of a root table
189
+ def root_table?() indent == 0 && dash.nil? && value.nil? end
190
+
191
+ # True if the line is the first line of a root record
192
+ def root_record?() indent == 0 && dash.nil? && !value.nil? || false end
193
+
194
+ # True if the line is the first line of a table element
195
+ def element?() !dash.nil? end
196
+
197
+ def to_s(long: false)
198
+ super +
199
+ if long
200
+ [ dash && "dash:#{dash.pos}",
201
+ ident && "ident:#{ident.pos} #{ident.value}",
202
+ value && "value:#{value.pos} #{value.value}",
203
+ reference && "reference:#{reference.pos} #{reference.value}",
204
+ anchor && "anchor:##{anchor.pos} #{anchor.value}"
205
+ ]
206
+ else
207
+ [ dash && "dash",
208
+ ident && "ident: #{ident.to_s}",
209
+ value && "value: #{value.to_s}",
210
+ reference && "reference: #{reference.to_s}",
211
+ anchor && "anchor: #{anchor.to_s}"
212
+ ]
213
+ end.compact.join(", ")
214
+ end
215
+ end
216
+ end
217
+
@@ -0,0 +1,153 @@
1
+
2
+ require 'pathname'
3
+
4
+ module FixtureFox
5
+ class Parser
6
+ attr_reader :file
7
+ attr_reader :ast
8
+ attr_reader :anchor_files # Name of external anchor files from @anchors directive
9
+
10
+ def initialize(file, lines, schema: nil)
11
+ @file = file
12
+ @lines = lines
13
+ @schema = schema || "public"
14
+ @anchor_files = []
15
+ end
16
+
17
+ def call(ast = nil)
18
+ @ast = ast || Ast.new(file)
19
+
20
+ # Current schema. The schema is initialized with a synthetic Ident token
21
+ # because the #analyzer needs a token to emit an error message if the
22
+ # public schema doesn't exist
23
+ schema = Ident.new(file, 1, peek.initial_indent, 1, @schema)
24
+
25
+ # Parse
26
+ get_line
27
+ while line
28
+ case line
29
+ when DirectiveLine
30
+ case line.directive.value
31
+ when "schema"
32
+ schema = line.argument
33
+ get_line
34
+ when "include"
35
+ parse_included_file(line.argument.value)
36
+ get_line
37
+ when "anchors" # TODO: Remove. Replaced by a command line option
38
+ @anchor_files << line.argument.value
39
+ get_line
40
+ end
41
+
42
+ when Line
43
+ if line.root_table? && line.empty
44
+ table = AstTable.new(@ast, schema, line.ident)
45
+ get_line
46
+ elsif line.root_table?
47
+ peek && peek.element? or line.error("Table definition expected")
48
+ table = AstTable.new(@ast, schema, line.ident)
49
+ get_line
50
+ parse_elements(table)
51
+ elsif line.root_record?
52
+ peek && peek.indent > 0 or line.error("Record definition expected")
53
+ # The tokenizer recognizes root records as fields (where the
54
+ # table name is tokenized as a Value - ie. a text). The following
55
+ # recreates the table name as a Token and also embeds the record
56
+ # in an AstTable. This effectively transforms a root-level record
57
+ # definition into a single-row table definition
58
+ # table_ident = Ident.new(
59
+ # file, line.value.lineno, line.initial_indent, line.value.pos, line.value.value)
60
+ name = PgGraph.inflector.record_type2table(line.value.value)
61
+ table_ident = Ident.new(
62
+ file, line.value.lineno, line.initial_indent, line.value.pos, name)
63
+ table = AstTable.new(@ast, schema, table_ident)
64
+ record = AstRecordElement.new(table, AnchorToken.of_ident(line.ident))
65
+ get_line
66
+ parse_members(record, line.indent)
67
+ else
68
+ line.error("Table or record definition expected")
69
+ end
70
+ else
71
+ raise "Oops"
72
+ end
73
+ end
74
+
75
+ @ast
76
+ end
77
+
78
+ private
79
+ def line() @line end
80
+ def get_line() @line = @lines.shift end
81
+ def peek() @lines.first end
82
+
83
+ def parse_included_file(path)
84
+ include_path = Pathname.new(path)
85
+ if include_path.absolute?
86
+ include_file = include_path.to_s
87
+ else
88
+ including_dir = Pathname.new(file).expand_path.dirname
89
+ include_file =
90
+ Pathname.new(including_dir.to_s + "/" + include_path.to_s)
91
+ .cleanpath
92
+ .relative_path_from(Pathname.getwd).to_s
93
+ end
94
+ tokenizer = Tokenizer.new(include_file)
95
+ Parser.new(tokenizer.file, tokenizer.call).call(@ast)
96
+ end
97
+
98
+ # Parse table elements. Current line should be the first element
99
+ def parse_elements(table)
100
+ element_indent = line.indent
101
+ while line && line.indent == element_indent && line.element?
102
+ if line.ident.nil? && line.reference
103
+ AstReferenceElement.new(table, line.reference)
104
+ get_line
105
+ elsif line.ident.nil? && line.anchor && !(peek && peek.indent > element_indent)
106
+ line.error("Empty record definition in table")
107
+ else
108
+ if !line.ident && line.anchor # - &label
109
+ record = AstRecordElement.new(table, line.anchor)
110
+ indent = line.anchor.pos - 1
111
+ get_line
112
+ parse_members(record, indent)
113
+ else # - key: value
114
+ record = AstRecordElement.new(table)
115
+ parse_members(record, line.ident.pos - 1, skip_first_check: true)
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ # Parse record members. Current line should be the first member
122
+ #
123
+ # If :skip_first_check the first check of indent is skipped. It is used for
124
+ # table rows where the first line in a record has a prefixed dash
125
+ def parse_members(record, indent, skip_first_check: false)
126
+ constrain indent, Integer
127
+ while line
128
+ break if !skip_first_check && line&.indent < indent # Record ends here if line is outdented
129
+ skip_first_check = false
130
+ if peek && peek.indent >= indent && peek.element? # This is a table if next line is dashed
131
+ table = AstTableMember.new(record, line.ident)
132
+ get_line
133
+ parse_elements(table)
134
+ elsif peek && peek.indent > indent # This is a record if next line is indented
135
+ record_member = AstRecordMember.new(record, line.ident, line.anchor)
136
+ get_line
137
+ parse_members(record_member, line.indent)
138
+ elsif line.anchor && !(peek && peek.indent > indent) # Empty record with a label
139
+ line.error("Empty record definition")
140
+ elsif line.anchor && line.dash && !(peek && peek.indent == indent)
141
+ line.error("Empty record definition 2")
142
+ elsif line.reference
143
+ AstReferenceMember.new(record, line.ident, line.reference)
144
+ get_line
145
+ else
146
+ AstFieldMember.new(record, line.ident, line.value || line.reference)
147
+ get_line
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+
@@ -0,0 +1,173 @@
1
+ module FixtureFox
2
+ class Token
3
+ attr_reader :file
4
+ attr_reader :lineno
5
+ attr_reader :initial_indent
6
+ attr_reader :pos
7
+ attr_reader :litt
8
+ attr_reader :value
9
+
10
+ def initialize(file, lineno, initial_indent, pos, litt)
11
+ @file, @lineno, @initial_indent, @pos, @litt = file, lineno, initial_indent, pos, litt
12
+ @value = @litt
13
+ end
14
+
15
+ def ==(s) @litt == s end
16
+ def to_s() @litt end
17
+
18
+ def error(msg)
19
+ raise ParseError.new(file, lineno, initial_indent + pos, msg)
20
+ end
21
+ end
22
+
23
+ class AnchorToken < Token
24
+ def initialize(file, lineno, initial_indent, pos, litt)
25
+ super
26
+ @value = litt[1..-1].to_sym
27
+ end
28
+
29
+ # Convert an Ident token to a AnchorToken token
30
+ def self.of_ident(ident)
31
+ AnchorToken.new(ident.file, ident.lineno, ident.initial_indent, ident.pos, "&#{ident.litt}")
32
+ end
33
+ end
34
+
35
+ class Empty < Token
36
+ end
37
+
38
+ class Directive < Token
39
+ def initialize(file, lineno, initial_indent, pos, litt)
40
+ super
41
+ @value = litt[1..-1]
42
+ end
43
+ end
44
+
45
+ class Ident < Token
46
+ end
47
+
48
+ class Reference < Token
49
+ # attr_reader :reference
50
+ def initialize(file, lineno, initial_indent, pos, litt)
51
+ super
52
+ @value = litt[1..-1]
53
+ end
54
+ end
55
+
56
+ class Value < Token
57
+ attr_reader :klass # NilClass, TrueClass, Integer, Float, String, Array
58
+
59
+ def initialize(file, lineno, initial_indent, pos, litt)
60
+ super
61
+
62
+ i = 1
63
+ case litt
64
+ when /^(nil)/, /^(null)/
65
+ i += $1.size
66
+ @klass = NilClass
67
+ @value = nil
68
+
69
+ when /^(true)/, /^(false)/
70
+ i += $1.size
71
+ @klass = TrueClass
72
+ @value = ($1 == "true")
73
+
74
+ when /^([-+]?\d+\.\d+)/
75
+ i += $1.size
76
+ @klass = Float
77
+ @value = $1.to_f
78
+
79
+ when /^([-+]?\d+)/
80
+ i += $1.size
81
+ @klass = Integer
82
+ @value = $1.to_i
83
+
84
+ when /^"/
85
+ @value = ""
86
+ @klass = String
87
+ loop do
88
+ i < litt.size or self.error("Unterminated string")
89
+ if litt[i] == "\\"
90
+ if litt[i+1] == "\\"
91
+ @value += "\\"
92
+ i += 2
93
+ elsif litt[i+1] == '"'
94
+ @value += '"'
95
+ i += 2
96
+ elsif litt[i+1] == 'n'
97
+ @value += "\n"
98
+ i += 2
99
+ elsif litt[i+1] == 't'
100
+ @value += "\t"
101
+ i += 2
102
+ else
103
+ @value += "\\"
104
+ i += 1
105
+ end
106
+ elsif litt[i] == '"'
107
+ i += 1
108
+ break
109
+ else
110
+ @value += litt[i]
111
+ i += 1
112
+ end
113
+ end
114
+
115
+ when /^'/
116
+ @value = ""
117
+ @klass = String
118
+ loop do
119
+ i < litt.size or self.error("Unterminated string")
120
+ if litt[i] == "'"
121
+ if litt[i+1] == "'"
122
+ @value += "'"
123
+ i += 2
124
+ else
125
+ i += 1
126
+ break
127
+ end
128
+ else
129
+ @value += litt[i]
130
+ i += 1
131
+ end
132
+ end
133
+
134
+ when /^(\[\s*)(.*)\]$/
135
+ i += $1.size
136
+ elements = $2
137
+ matches = elements.split(/(\s*(?<!(?<!\\{2})\\),\s*)/)
138
+
139
+ @value = []
140
+ while element = matches.shift
141
+ unescaped_element = element.sub(/\\/, "")
142
+ @value << Value.new(file, lineno, initial_indent, i, unescaped_element).value
143
+ i += element.size + (matches.shift&.size || 0)
144
+ end
145
+ @klass = Array
146
+
147
+ when /^\{/
148
+ i += litt.size
149
+ @value = HashParser.parse(litt)
150
+ @klass = Hash
151
+
152
+ else
153
+ i += litt.size
154
+ @klass = String
155
+ @value = litt.dup
156
+ end
157
+
158
+ (litt[i..-1] || "") =~ /^\s*(?:#.*)?$/ or self.error("Illegal characters after literal")
159
+ end
160
+
161
+ def to_s()
162
+ if klass == NilClass
163
+ "null"
164
+ # elsif klass == String
165
+ # "'#{value.gsub("'", "''")}'"
166
+ else
167
+ value.to_s
168
+ end
169
+ end
170
+ end
171
+
172
+ end
173
+