kdl 0.1.2 → 1.0.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.
data/lib/kdl/kdl.yy CHANGED
@@ -4,59 +4,76 @@ class KDL::Parser
4
4
  STRING RAWSTRING
5
5
  INTEGER FLOAT TRUE FALSE NULL
6
6
  WS NEWLINE
7
+ LBRACE RBRACE
7
8
  LPAREN RPAREN
8
9
  EQUALS
9
10
  SEMICOLON
10
11
  EOF
11
12
  SLASHDASH
13
+ ESCLINE
12
14
  rule
13
15
  document : nodes { KDL::Document.new(val[0]) }
14
16
  | linespaces { KDL::Document.new([])}
15
17
 
16
- nodes : none { [] }
17
- | linespace_star node { [val[1]] }
18
- | linespace_star empty_node { [] }
19
- | nodes node { [*val[0], val[1]] }
20
- | nodes empty_node { val[0] }
21
- node : node_decl node_term { val[0] }
22
- | node_decl node_children node_term { val[0].tap { |x| x.children = val[1] } }
23
- | node_decl empty_children node_term { val[0] }
24
- node_decl : identifier { KDL::Node.new(val[0]) }
25
- | node_decl WS value { val[0].tap { |x| x.arguments << val[2] } }
26
- | node_decl WS SLASHDASH ws_star value { val[0] }
27
- | node_decl WS property { val[0].tap { |x| x.properties[val[2][0]] = val[2][1] } }
28
- | node_decl WS SLASHDASH ws_star property { val[0] }
29
- empty_node: SLASHDASH ws_star node
30
- node_children: ws_star LPAREN nodes RPAREN { val[2] }
18
+ nodes : none { [] }
19
+ | linespaces node { [val[1]] }
20
+ | linespaces empty_node { [] }
21
+ | nodes node { [*val[0], val[1]] }
22
+ | nodes empty_node { val[0] }
23
+ node : untyped_node { val[0] }
24
+ | type untyped_node { val[1].as_type(val[0], @type_parsers.fetch(val[0], nil)) }
25
+ untyped_node : node_decl node_term { val[0].tap { |x| x.children = [] } }
26
+ | node_decl node_children node_term { val[0].tap { |x| x.children = val[1] } }
27
+ | node_decl empty_children node_term { val[0].tap { |x| x.children = [] } }
28
+ node_decl : identifier { KDL::Node.new(val[0]) }
29
+ | node_decl ws value { val[0].tap { |x| x.arguments << val[2] } }
30
+ | node_decl ws SLASHDASH ws_star value { val[0] }
31
+ | node_decl ws property { val[0].tap { |x| x.properties[val[2][0]] = val[2][1] } }
32
+ | node_decl ws SLASHDASH ws_star property { val[0] }
33
+ empty_node : SLASHDASH ws_star node
34
+ node_children : ws_star LBRACE nodes RBRACE { val[2] }
35
+ | ws_star LBRACE linespaces RBRACE { [] }
31
36
  empty_children: SLASHDASH node_children
32
- | WS empty_children
37
+ | ws empty_children
33
38
  node_term: linespaces | semicolon_term
34
39
  semicolon_term: SEMICOLON | SEMICOLON linespaces
35
40
 
36
- identifier: IDENT { val[0].value }
37
- | STRING { val[0].value }
41
+ type : LPAREN identifier RPAREN { val[1] }
42
+
43
+ identifier: IDENT { val[0].value }
44
+ | STRING { val[0].value }
45
+ | RAWSTRING { val[0].value }
38
46
 
39
47
  property: identifier EQUALS value { [val[0], val[2]] }
40
48
 
41
- value : STRING { KDL::Value::String.new(val[0].value) }
42
- | RAWSTRING { KDL::Value::String.new(val[0].value) }
43
- | INTEGER { KDL::Value::Int.new(val[0].value) }
44
- | FLOAT { KDL::Value::Float.new(val[0].value) }
45
- | boolean { KDL::Value::Boolean.new(val[0]) }
46
- | NULL { KDL::Value::Null }
49
+ value : untyped_value
50
+ | type untyped_value { val[1].as_type(val[0], @type_parsers.fetch(val[0], nil)) }
51
+
52
+ untyped_value : STRING { KDL::Value::String.new(val[0].value) }
53
+ | RAWSTRING { KDL::Value::String.new(val[0].value) }
54
+ | INTEGER { KDL::Value::Int.new(val[0].value) }
55
+ | FLOAT { KDL::Value::Float.new(val[0].value, format: val[0].meta[:format]) }
56
+ | boolean { KDL::Value::Boolean.new(val[0]) }
57
+ | NULL { KDL::Value::Null }
47
58
 
48
59
  boolean : TRUE { true }
49
60
  | FALSE { false }
50
61
 
51
- ws_star: none | WS
62
+ ws: WS | ESCLINE | WS ESCLINE | ESCLINE WS | WS ESCLINE WS
63
+ ws_star: none | ws
52
64
  linespace: WS | NEWLINE | EOF
53
65
  linespaces: linespace | linespaces linespace
54
- linespace_star: none | linespaces
55
66
 
56
67
  none: { nil }
57
68
 
58
69
  ---- inner
59
- def parse(str)
70
+
71
+ def parse(str, options = {})
72
+ if options.fetch(:parse_types, true)
73
+ @type_parsers = ::KDL::Types::MAPPING.merge(options.fetch(:type_parsers, {}))
74
+ else
75
+ @type_parsers = {}
76
+ end
60
77
  @tokenizer = ::KDL::Tokenizer.new(str)
61
78
  do_parse
62
79
  end
data/lib/kdl/node.rb CHANGED
@@ -1,31 +1,31 @@
1
1
  module KDL
2
2
  class Node
3
- attr_accessor :name, :arguments, :properties, :children
3
+ attr_accessor :name, :arguments, :properties, :children, :type
4
4
 
5
- def initialize(name, arguments = [], properties = {}, children = [])
5
+ def initialize(name, arguments = [], properties = {}, children = [], type: nil)
6
6
  @name = name
7
7
  @arguments = arguments
8
8
  @properties = properties
9
9
  @children = children
10
+ @type = type
10
11
  end
11
12
 
12
13
  def to_s(level = 0)
13
14
  indent = ' ' * level
14
- s = "#{indent}#{name}"
15
+ s = "#{indent}#{type ? "(#{id_to_s type})" : ''}#{id_to_s name}"
15
16
  unless arguments.empty?
16
17
  s += " #{arguments.map(&:to_s).join(' ')}"
17
18
  end
18
19
  unless properties.empty?
19
- s += " #{properties.map { |k, v| "#{k}=#{v}" }.join(' ')}"
20
+ s += " #{properties.map { |k, v| "#{id_to_s k}=#{v}" }.join(' ')}"
20
21
  end
21
22
  unless children.empty?
22
23
  s += " {\n"
23
- s += children.map { |c| c.to_s(level + 1) }.join("\n")
24
- s += "\n#{indent}}"
24
+ s += children.map { |c| "#{c.to_s(level + 1)}\n" }.join("\n")
25
+ s += "#{indent}}"
25
26
  end
26
27
  s
27
28
  end
28
- alias inspect to_s
29
29
 
30
30
  def ==(other)
31
31
  return false unless other.is_a?(Node)
@@ -35,5 +35,28 @@ module KDL
35
35
  properties == other.properties &&
36
36
  children == other.children
37
37
  end
38
+
39
+ def as_type(type, parser = nil)
40
+ if parser.nil?
41
+ @type = type
42
+ self
43
+ else
44
+ result = parser.call(self, type)
45
+
46
+ return self.as_type(type) if result.nil?
47
+
48
+ unless result.is_a?(::KDL::Node)
49
+ raise ArgumentError, "expected parser to return an instance of ::KDL::Node, got `#{result.class}'"
50
+ end
51
+
52
+ result
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def id_to_s(id)
59
+ StringDumper.stringify_identifier(id)
60
+ end
38
61
  end
39
62
  end
@@ -0,0 +1,45 @@
1
+ module KDL
2
+ module StringDumper
3
+ class << self
4
+ def call(string)
5
+ %("#{string.each_char.map { |char| escape(char) }.join}")
6
+ end
7
+
8
+ def stringify_identifier(ident)
9
+ if bare_identifier?(ident)
10
+ ident
11
+ else
12
+ call(ident)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def print?(char)
19
+ ' ' <= char && char <= '\x7e'
20
+ end
21
+
22
+ def escape(char)
23
+ case char
24
+ when "\n" then '\n'
25
+ when "\r" then '\r'
26
+ when "\t" then '\t'
27
+ when '\\' then '\\\\'
28
+ when '"' then '\"'
29
+ when "\b" then '\b'
30
+ when "\f" then '\f'
31
+ else char
32
+ end
33
+ end
34
+
35
+ def unicode_escape(char)
36
+ "\\u{#{char.codepoints.first.to_s(16)}}"
37
+ end
38
+
39
+ def bare_identifier?(name)
40
+ escape_chars = '\\\/(){}<>;\[\]=,"'
41
+ name =~ /^([^0-9\-+\s#{escape_chars}][^\s#{escape_chars}]*|[\-+](?!true|false|null)[^0-9\s#{escape_chars}][^\s#{escape_chars}]*)$/
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/kdl/tokenizer.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'bigdecimal'
2
+
1
3
  module KDL
2
4
  class Tokenizer
3
5
  class Error < StandardError
@@ -7,13 +9,14 @@ module KDL
7
9
  end
8
10
 
9
11
  class Token
10
- attr_reader :type, :value, :line, :column
12
+ attr_reader :type, :value, :line, :column, :meta
11
13
 
12
- def initialize(type, value, line, column)
14
+ def initialize(type, value, line, column, meta = {})
13
15
  @type = type
14
16
  @value = value
15
17
  @line = line
16
18
  @column = column
19
+ @meta = meta
17
20
  end
18
21
 
19
22
  def ==(other)
@@ -31,8 +34,8 @@ module KDL
31
34
  attr_reader :index
32
35
 
33
36
  SYMBOLS = {
34
- '{' => :LPAREN,
35
- '}' => :RPAREN,
37
+ '{' => :LBRACE,
38
+ '}' => :RBRACE,
36
39
  '=' => :EQUALS,
37
40
  '=' => :EQUALS,
38
41
  ';' => :SEMICOLON
@@ -46,10 +49,13 @@ module KDL
46
49
 
47
50
  NEWLINES = ["\u000A", "\u0085", "\u000C", "\u2028", "\u2029"]
48
51
 
49
- NON_IDENTIFIER_CHARS = Regexp.escape "#{SYMBOLS.keys.join('')}\\<>[]\","
52
+ NON_IDENTIFIER_CHARS = Regexp.escape "#{SYMBOLS.keys.join('')}()/\\<>[]\","
50
53
  IDENTIFIER_CHARS = /[^#{NON_IDENTIFIER_CHARS}\x0-\x20]/
51
54
  INITIAL_IDENTIFIER_CHARS = /[^#{NON_IDENTIFIER_CHARS}0-9\x0-\x20]/
52
55
 
56
+ ALLOWED_IN_TYPE = [:ident, :string, :rawstring]
57
+ NOT_ALLOWED_AFTER_TYPE = [:single_line_comment, :multi_line_comment]
58
+
53
59
  def initialize(str, start = 0)
54
60
  @str = str
55
61
  @context = nil
@@ -60,6 +66,8 @@ module KDL
60
66
  @previous_context = nil
61
67
  @line = 1
62
68
  @column = 1
69
+ @type_context = false
70
+ @last_token = nil
63
71
  end
64
72
 
65
73
  def next_token
@@ -117,15 +125,13 @@ module KDL
117
125
  end
118
126
  when '\\'
119
127
  t = Tokenizer.new(@str, @index + 1)
120
- la = t.next_token[0]
121
- if la == :NEWLINE
122
- @index = t.index
123
- new_line
124
- elsif la == :WS && (lan = t.next_token[0]) == :NEWLINE
128
+ la = t.next_token
129
+ if la[0] == :NEWLINE || la[0] == :EOF || (la[0] == :WS && (lan = t.next_token[0]) == :NEWLINE || lan == :EOF)
125
130
  @index = t.index
126
131
  new_line
132
+ return token(:ESCLINE, "\\#{la[1].value}")
127
133
  else
128
- raise_error "Unexpected '\\'"
134
+ raise_error "Unexpected '\\' (#{la[0]})"
129
135
  end
130
136
  when *SYMBOLS.keys
131
137
  return token(SYMBOLS[c], c).tap { traverse(1) }
@@ -174,6 +180,12 @@ module KDL
174
180
  self.context = :ident
175
181
  @buffer = c
176
182
  traverse(1)
183
+ when '('
184
+ @type_context = true
185
+ return token(:LPAREN, c).tap { traverse(1) }
186
+ when ')'
187
+ @type_context = false
188
+ return token(:RPAREN, c).tap { traverse(1) }
177
189
  else
178
190
  raise_error "Unexpected character #{c.inspect}"
179
191
  end
@@ -279,18 +291,6 @@ module KDL
279
291
  if WHITEPACE.include?(c)
280
292
  traverse(1)
281
293
  @buffer += c
282
- elsif c == "\\"
283
- t = Tokenizer.new(@str, @index + 1)
284
- la = t.next_token[0]
285
- if la == :NEWLINE
286
- @index = t.index
287
- new_line
288
- elsif (la == :WS && (lan = t.next_token[0]) == :NEWLINE)
289
- @index = t.index
290
- new_line
291
- else
292
- raise_error "Unexpected '\\'"
293
- end
294
294
  elsif c == "/" && @str[@index + 1] == '*'
295
295
  self.context = :multi_line_comment
296
296
  @comment_nesting = 1
@@ -304,8 +304,8 @@ module KDL
304
304
 
305
305
  private
306
306
 
307
- def token(type, value)
308
- [type, Token.new(type, value, @line_at_start, @column_at_start)]
307
+ def token(type, value, **meta)
308
+ @last_token = [type, Token.new(type, value, @line_at_start, @column_at_start, meta)]
309
309
  end
310
310
 
311
311
  def traverse(n = 1)
@@ -323,6 +323,11 @@ module KDL
323
323
  end
324
324
 
325
325
  def context=(val)
326
+ if @type_context && !ALLOWED_IN_TYPE.include?(val)
327
+ raise_error "#{val} context not allowed in type declaration"
328
+ elsif @last_token && @last_token[0] == :RPAREN && NOT_ALLOWED_AFTER_TYPE.include?(val)
329
+ raise_error 'Comments are not allowed after a type declaration'
330
+ end
326
331
  @previous_context = @context
327
332
  @context = val
328
333
  end
@@ -333,18 +338,35 @@ module KDL
333
338
  end
334
339
 
335
340
  def parse_decimal(s)
336
- return token(:FLOAT, Float(munch_underscores(s))) if s =~ /[.eE]/
337
- token(:INTEGER, Integer(munch_underscores(s), 10))
341
+ return parse_float(s) if s =~ /[.E]/i
342
+
343
+ token(:INTEGER, Integer(munch_underscores(s), 10), format: '%d')
344
+ end
345
+
346
+ def parse_float(s)
347
+ match, _, fraction, exponent = *s.match(/^([-+]?[\d_]+)(?:\.([\d_]+))?(?:[eE]([-+]?[\d_]+))?$/)
348
+ raise_error "Invalid floating point value #{s}" if match.nil?
349
+
350
+ s = munch_underscores(s)
351
+
352
+ decimals = fraction.nil? ? 0 : fraction.size
353
+ value = Float(s)
354
+ scientific = value.abs >= 100 || (exponent && exponent.to_i.abs >= 2)
355
+ if value.infinite? || (value.zero? && exponent.to_i < 0)
356
+ token(:FLOAT, BigDecimal(s))
357
+ else
358
+ token(:FLOAT, value, format: scientific ? "%.#{decimals}E" : nil)
359
+ end
338
360
  end
339
-
361
+
340
362
  def parse_hexadecimal(s)
341
363
  token(:INTEGER, Integer(munch_underscores(s), 16))
342
364
  end
343
-
365
+
344
366
  def parse_octal(s)
345
367
  token(:INTEGER, Integer(munch_underscores(s), 8))
346
368
  end
347
-
369
+
348
370
  def parse_binary(s)
349
371
  token(:INTEGER, Integer(munch_underscores(s), 2))
350
372
  end
@@ -0,0 +1,15 @@
1
+ require 'base64'
2
+
3
+ module KDL
4
+ module Types
5
+ class Base64 < Value
6
+ def self.call(value, type = 'base64')
7
+ return nil unless value.is_a? ::KDL::Value::String
8
+
9
+ data = ::Base64.decode64(value.value)
10
+ new(data, type: type)
11
+ end
12
+ end
13
+ MAPPING['base64'] = Base64
14
+ end
15
+ end