fixture_fox 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+