cooklang 0.1.0 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +35 -0
- data/.gitignore +12 -0
- data/.qlty/.gitignore +7 -0
- data/.qlty/configs/.yamllint.yaml +21 -0
- data/.qlty/qlty.toml +101 -0
- data/.rspec +3 -0
- data/.rubocop.yml +289 -75
- data/Gemfile.lock +65 -26
- data/{LICENSE → LICENSE.txt} +6 -6
- data/README.md +106 -12
- data/Rakefile +5 -1
- data/cooklang.gemspec +35 -0
- data/lib/cooklang/builders/recipe_builder.rb +64 -0
- data/lib/cooklang/builders/step_builder.rb +76 -0
- data/lib/cooklang/cookware.rb +43 -0
- data/lib/cooklang/formatter.rb +61 -0
- data/lib/cooklang/formatters/text.rb +18 -0
- data/lib/cooklang/ingredient.rb +60 -0
- data/lib/cooklang/lexer.rb +282 -0
- data/lib/cooklang/metadata.rb +98 -0
- data/lib/cooklang/note.rb +27 -0
- data/lib/cooklang/parser.rb +41 -0
- data/lib/cooklang/parsers/cookware_parser.rb +133 -0
- data/lib/cooklang/parsers/ingredient_parser.rb +179 -0
- data/lib/cooklang/parsers/timer_parser.rb +135 -0
- data/lib/cooklang/processors/element_parser.rb +45 -0
- data/lib/cooklang/processors/metadata_processor.rb +129 -0
- data/lib/cooklang/processors/step_processor.rb +208 -0
- data/lib/cooklang/processors/token_processor.rb +104 -0
- data/lib/cooklang/recipe.rb +72 -0
- data/lib/cooklang/section.rb +33 -0
- data/lib/cooklang/step.rb +99 -0
- data/lib/cooklang/timer.rb +65 -0
- data/lib/cooklang/token_stream.rb +130 -0
- data/lib/cooklang/version.rb +1 -1
- data/lib/cooklang.rb +22 -1
- data/spec/comprehensive_spec.rb +179 -0
- data/spec/cooklang_spec.rb +38 -0
- data/spec/fixtures/canonical.yaml +837 -0
- data/spec/formatters/text_spec.rb +189 -0
- data/spec/integration/canonical_spec.rb +211 -0
- data/spec/lexer_spec.rb +357 -0
- data/spec/models/cookware_spec.rb +116 -0
- data/spec/models/ingredient_spec.rb +192 -0
- data/spec/models/metadata_spec.rb +241 -0
- data/spec/models/note_spec.rb +65 -0
- data/spec/models/recipe_spec.rb +171 -0
- data/spec/models/section_spec.rb +65 -0
- data/spec/models/step_spec.rb +236 -0
- data/spec/models/timer_spec.rb +173 -0
- data/spec/parser_spec.rb +398 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/token_stream_spec.rb +278 -0
- metadata +141 -24
- data/.ruby-version +0 -1
- data/CHANGELOG.md +0 -5
- data/bin/console +0 -15
- data/bin/setup +0 -8
@@ -0,0 +1,282 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "strscan"
|
4
|
+
|
5
|
+
module Cooklang
|
6
|
+
Token = Struct.new(:type, :value, :position, :line, :column) do
|
7
|
+
def initialize(type, value, position = 0, line = 1, column = 1)
|
8
|
+
super
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Lexer
|
13
|
+
TOKENS = {
|
14
|
+
ingredient_marker: "@",
|
15
|
+
cookware_marker: "#",
|
16
|
+
timer_marker: "~",
|
17
|
+
open_brace: "{",
|
18
|
+
close_brace: "}",
|
19
|
+
open_paren: "(",
|
20
|
+
close_paren: ")",
|
21
|
+
percent: "%",
|
22
|
+
comment_line: "--",
|
23
|
+
comment_block_start: "[-",
|
24
|
+
comment_block_end: "-]",
|
25
|
+
metadata_marker: ">>",
|
26
|
+
section_marker: "=",
|
27
|
+
note_marker: ">",
|
28
|
+
newline: "\n",
|
29
|
+
yaml_delimiter: "---"
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
def initialize(input)
|
33
|
+
@input = input
|
34
|
+
@scanner = StringScanner.new(input)
|
35
|
+
@line = 1
|
36
|
+
@column = 1
|
37
|
+
@tokens = []
|
38
|
+
end
|
39
|
+
|
40
|
+
def tokenize
|
41
|
+
@tokens = []
|
42
|
+
|
43
|
+
until @scanner.eos?
|
44
|
+
if match_yaml_delimiter
|
45
|
+
elsif match_comment_block
|
46
|
+
elsif match_comment_line
|
47
|
+
elsif match_metadata_marker
|
48
|
+
elsif match_section_marker
|
49
|
+
elsif match_note_marker
|
50
|
+
elsif match_special_chars
|
51
|
+
elsif match_newline
|
52
|
+
elsif match_text
|
53
|
+
elsif match_hyphen
|
54
|
+
else
|
55
|
+
# Skip unrecognized character
|
56
|
+
advance_position(@scanner.getch)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
@tokens
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
def current_position
|
65
|
+
@scanner.pos
|
66
|
+
end
|
67
|
+
|
68
|
+
def current_line
|
69
|
+
@line
|
70
|
+
end
|
71
|
+
|
72
|
+
def current_column
|
73
|
+
@column
|
74
|
+
end
|
75
|
+
|
76
|
+
def advance_position(text)
|
77
|
+
text.each_char do |char|
|
78
|
+
if char == "\n"
|
79
|
+
@line += 1
|
80
|
+
@column = 1
|
81
|
+
else
|
82
|
+
@column += 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def add_token(type, value)
|
88
|
+
position = current_position
|
89
|
+
line = current_line
|
90
|
+
column = current_column
|
91
|
+
@tokens << Token.new(type, value, position, line, column)
|
92
|
+
advance_position(value)
|
93
|
+
end
|
94
|
+
|
95
|
+
def capture_single_char_token(type)
|
96
|
+
position = current_position
|
97
|
+
line = current_line
|
98
|
+
column = current_column
|
99
|
+
value = @scanner.getch
|
100
|
+
@tokens << Token.new(type, value, position, line, column)
|
101
|
+
advance_position(value)
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
def match_yaml_delimiter
|
106
|
+
if @scanner.check(/^---/)
|
107
|
+
position = current_position
|
108
|
+
line = current_line
|
109
|
+
column = current_column
|
110
|
+
value = @scanner.scan("---")
|
111
|
+
@tokens << Token.new(:yaml_delimiter, value, position, line, column)
|
112
|
+
advance_position(value)
|
113
|
+
true
|
114
|
+
else
|
115
|
+
false
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def match_comment_block
|
120
|
+
if @scanner.check(/\[-/)
|
121
|
+
position = current_position
|
122
|
+
line = current_line
|
123
|
+
column = current_column
|
124
|
+
@scanner.scan("[-")
|
125
|
+
@tokens << Token.new(:comment_block_start, "[-", position, line, column)
|
126
|
+
advance_position("[-")
|
127
|
+
|
128
|
+
# Scan until block end or EOF
|
129
|
+
content = ""
|
130
|
+
content += @scanner.getch while !@scanner.eos? && !@scanner.check(/-\]/)
|
131
|
+
|
132
|
+
add_token(:text, content) unless content.empty?
|
133
|
+
|
134
|
+
if @scanner.check(/-\]/)
|
135
|
+
position = current_position
|
136
|
+
line = current_line
|
137
|
+
column = current_column
|
138
|
+
@scanner.scan("-]")
|
139
|
+
@tokens << Token.new(:comment_block_end, "-]", position, line, column)
|
140
|
+
advance_position("-]")
|
141
|
+
end
|
142
|
+
|
143
|
+
true
|
144
|
+
else
|
145
|
+
false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def match_comment_line
|
150
|
+
if @scanner.check(/--/)
|
151
|
+
position = current_position
|
152
|
+
line = current_line
|
153
|
+
column = current_column
|
154
|
+
value = @scanner.scan("--")
|
155
|
+
@tokens << Token.new(:comment_line, value, position, line, column)
|
156
|
+
advance_position(value)
|
157
|
+
|
158
|
+
# Scan rest of line
|
159
|
+
line_content = @scanner.scan(/[^\n]*/)
|
160
|
+
add_token(:text, line_content) if line_content && !line_content.empty?
|
161
|
+
|
162
|
+
true
|
163
|
+
else
|
164
|
+
false
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def match_metadata_marker
|
169
|
+
if @scanner.check(/>>/)
|
170
|
+
position = current_position
|
171
|
+
line = current_line
|
172
|
+
column = current_column
|
173
|
+
value = @scanner.scan(">>")
|
174
|
+
@tokens << Token.new(:metadata_marker, value, position, line, column)
|
175
|
+
advance_position(value)
|
176
|
+
true
|
177
|
+
else
|
178
|
+
false
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def match_section_marker
|
183
|
+
if @scanner.check(/=+/)
|
184
|
+
position = current_position
|
185
|
+
line = current_line
|
186
|
+
column = current_column
|
187
|
+
value = @scanner.scan(/=+/)
|
188
|
+
@tokens << Token.new(:section_marker, value, position, line, column)
|
189
|
+
advance_position(value)
|
190
|
+
true
|
191
|
+
else
|
192
|
+
false
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def match_note_marker
|
197
|
+
# Only match > if it's not part of >>
|
198
|
+
if @scanner.check(/>/) && !@scanner.check(/>>/)
|
199
|
+
position = current_position
|
200
|
+
line = current_line
|
201
|
+
column = current_column
|
202
|
+
value = @scanner.scan(">")
|
203
|
+
@tokens << Token.new(:note_marker, value, position, line, column)
|
204
|
+
advance_position(value)
|
205
|
+
true
|
206
|
+
else
|
207
|
+
false
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def match_special_chars
|
212
|
+
char = @scanner.check(/./)
|
213
|
+
|
214
|
+
case char
|
215
|
+
when "@"
|
216
|
+
capture_single_char_token(:ingredient_marker)
|
217
|
+
when "#"
|
218
|
+
capture_single_char_token(:cookware_marker)
|
219
|
+
when "~"
|
220
|
+
capture_single_char_token(:timer_marker)
|
221
|
+
when "{"
|
222
|
+
capture_single_char_token(:open_brace)
|
223
|
+
when "}"
|
224
|
+
capture_single_char_token(:close_brace)
|
225
|
+
when "("
|
226
|
+
capture_single_char_token(:open_paren)
|
227
|
+
when ")"
|
228
|
+
capture_single_char_token(:close_paren)
|
229
|
+
when "%"
|
230
|
+
capture_single_char_token(:percent)
|
231
|
+
else
|
232
|
+
false
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def match_newline
|
237
|
+
if @scanner.check(/\n/)
|
238
|
+
position = current_position
|
239
|
+
line = current_line
|
240
|
+
column = current_column
|
241
|
+
value = @scanner.scan("\n")
|
242
|
+
@tokens << Token.new(:newline, value, position, line, column)
|
243
|
+
advance_position(value)
|
244
|
+
true
|
245
|
+
else
|
246
|
+
false
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def match_text
|
251
|
+
# Match any printable text that's not a special character, including spaces and tabs
|
252
|
+
# Exclude [ and ] to allow block comment detection
|
253
|
+
# Exclude = and > to allow section and note markers
|
254
|
+
# Include tabs explicitly along with printable characters
|
255
|
+
if @scanner.check(/[\t[:print:]&&[^@#~{}()%\n\-\[\]=>]]+/)
|
256
|
+
position = current_position
|
257
|
+
line = current_line
|
258
|
+
column = current_column
|
259
|
+
text = @scanner.scan(/[\t[:print:]&&[^@#~{}()%\n\-\[\]=>]]+/)
|
260
|
+
@tokens << Token.new(:text, text, position, line, column)
|
261
|
+
advance_position(text)
|
262
|
+
true
|
263
|
+
else
|
264
|
+
false
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def match_hyphen
|
269
|
+
if @scanner.check(/-/) && !@scanner.check(/--/)
|
270
|
+
position = current_position
|
271
|
+
line = current_line
|
272
|
+
column = current_column
|
273
|
+
@scanner.scan("-")
|
274
|
+
@tokens << Token.new(:hyphen, "-", position, line, column)
|
275
|
+
advance_position("-")
|
276
|
+
true
|
277
|
+
else
|
278
|
+
false
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cooklang
|
4
|
+
class Metadata < Hash
|
5
|
+
def initialize(data = {})
|
6
|
+
super()
|
7
|
+
data.each { |key, value| self[key.to_s] = value }
|
8
|
+
end
|
9
|
+
|
10
|
+
def []=(key, value)
|
11
|
+
super(key.to_s, value)
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](key)
|
15
|
+
super(key.to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
def key?(key)
|
19
|
+
super(key.to_s)
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete(key)
|
23
|
+
super(key.to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
def fetch(key, *)
|
27
|
+
super(key.to_s, *)
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_h
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def servings
|
35
|
+
self["servings"]&.to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
def servings=(value)
|
39
|
+
self["servings"] = value
|
40
|
+
end
|
41
|
+
|
42
|
+
def prep_time
|
43
|
+
self["prep_time"] || self["prep-time"]
|
44
|
+
end
|
45
|
+
|
46
|
+
def prep_time=(value)
|
47
|
+
self["prep_time"] = value
|
48
|
+
end
|
49
|
+
|
50
|
+
def cook_time
|
51
|
+
self["cook_time"] || self["cook-time"]
|
52
|
+
end
|
53
|
+
|
54
|
+
def cook_time=(value)
|
55
|
+
self["cook_time"] = value
|
56
|
+
end
|
57
|
+
|
58
|
+
def total_time
|
59
|
+
self["total_time"] || self["total-time"]
|
60
|
+
end
|
61
|
+
|
62
|
+
def total_time=(value)
|
63
|
+
self["total_time"] = value
|
64
|
+
end
|
65
|
+
|
66
|
+
def title
|
67
|
+
self["title"]
|
68
|
+
end
|
69
|
+
|
70
|
+
def title=(value)
|
71
|
+
self["title"] = value
|
72
|
+
end
|
73
|
+
|
74
|
+
def source
|
75
|
+
self["source"]
|
76
|
+
end
|
77
|
+
|
78
|
+
def source=(value)
|
79
|
+
self["source"] = value
|
80
|
+
end
|
81
|
+
|
82
|
+
def tags
|
83
|
+
value = self["tags"]
|
84
|
+
case value
|
85
|
+
when Array
|
86
|
+
value
|
87
|
+
when String
|
88
|
+
value.split(",").map(&:strip)
|
89
|
+
else
|
90
|
+
[]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def tags=(value)
|
95
|
+
self["tags"] = value
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cooklang
|
4
|
+
class Note
|
5
|
+
attr_reader :content
|
6
|
+
|
7
|
+
def initialize(content:)
|
8
|
+
@content = content.to_s.freeze
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
content
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_h
|
16
|
+
{ content: content }
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(other)
|
20
|
+
other.is_a?(Note) && content == other.content
|
21
|
+
end
|
22
|
+
|
23
|
+
def hash
|
24
|
+
content.hash
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lexer"
|
4
|
+
require_relative "processors/metadata_processor"
|
5
|
+
require_relative "processors/token_processor"
|
6
|
+
require_relative "processors/step_processor"
|
7
|
+
require_relative "builders/recipe_builder"
|
8
|
+
|
9
|
+
module Cooklang
|
10
|
+
class Parser
|
11
|
+
def parse(input)
|
12
|
+
# Tokenize input
|
13
|
+
lexer = Lexer.new(input)
|
14
|
+
tokens = lexer.tokenize
|
15
|
+
|
16
|
+
# Extract metadata
|
17
|
+
metadata, content_tokens = Processors::MetadataProcessor.extract_metadata(tokens)
|
18
|
+
|
19
|
+
# Clean up tokens
|
20
|
+
cleaned_tokens = Processors::TokenProcessor.strip_comments(content_tokens)
|
21
|
+
notes, recipe_tokens = Processors::TokenProcessor.extract_notes(cleaned_tokens)
|
22
|
+
|
23
|
+
# Parse steps
|
24
|
+
parsed_steps = Processors::StepProcessor.parse_steps(recipe_tokens)
|
25
|
+
|
26
|
+
# Build final recipe
|
27
|
+
recipe = Builders::RecipeBuilder.build_recipe(parsed_steps, metadata)
|
28
|
+
|
29
|
+
# Add notes to recipe (create new recipe with notes)
|
30
|
+
Recipe.new(
|
31
|
+
ingredients: recipe.ingredients,
|
32
|
+
cookware: recipe.cookware,
|
33
|
+
timers: recipe.timers,
|
34
|
+
steps: recipe.steps,
|
35
|
+
metadata: recipe.metadata,
|
36
|
+
sections: recipe.sections,
|
37
|
+
notes: notes
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../cookware"
|
4
|
+
require_relative "../token_stream"
|
5
|
+
|
6
|
+
module Cooklang
|
7
|
+
module Parsers
|
8
|
+
class CookwareParser
|
9
|
+
def initialize(stream)
|
10
|
+
@stream = stream
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse
|
14
|
+
return nil unless @stream.current&.type == :cookware_marker
|
15
|
+
@stream.consume(:cookware_marker) # Skip the # marker
|
16
|
+
|
17
|
+
return nil if invalid_syntax?
|
18
|
+
|
19
|
+
brace_index = find_next_brace
|
20
|
+
has_valid_brace = brace_index && !brace_belongs_to_other_marker?(brace_index)
|
21
|
+
|
22
|
+
if has_valid_brace
|
23
|
+
parse_braced_cookware(brace_index)
|
24
|
+
else
|
25
|
+
parse_simple_cookware
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def invalid_syntax?
|
31
|
+
@stream.current&.type == :text && @stream.current.value.start_with?(" ")
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_next_brace
|
35
|
+
@stream.find_next(:open_brace)
|
36
|
+
end
|
37
|
+
|
38
|
+
def brace_belongs_to_other_marker?(brace_index)
|
39
|
+
current_pos = @stream.position
|
40
|
+
|
41
|
+
while @stream.position < brace_index && !@stream.eof?
|
42
|
+
if %i[ingredient_marker cookware_marker timer_marker].include?(@stream.current.type)
|
43
|
+
@stream.advance_to(current_pos)
|
44
|
+
return true
|
45
|
+
end
|
46
|
+
@stream.consume
|
47
|
+
end
|
48
|
+
|
49
|
+
@stream.advance_to(current_pos)
|
50
|
+
false
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse_braced_cookware(brace_index)
|
54
|
+
name = extract_name_until_brace(brace_index)
|
55
|
+
@stream.consume(:open_brace) # Skip open brace
|
56
|
+
|
57
|
+
quantity = extract_quantity
|
58
|
+
@stream.consume(:close_brace) # Skip close brace
|
59
|
+
|
60
|
+
quantity = 1 if quantity.nil? || quantity == ""
|
61
|
+
|
62
|
+
Cookware.new(name: name, quantity: quantity)
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse_simple_cookware
|
66
|
+
remaining_text = nil
|
67
|
+
|
68
|
+
if @stream.current&.type == :text
|
69
|
+
text = @stream.current.value
|
70
|
+
if text.match(/^([a-zA-Z0-9_]+)(.*)$/)
|
71
|
+
name = ::Regexp.last_match(1)
|
72
|
+
remaining_text = ::Regexp.last_match(2)
|
73
|
+
else
|
74
|
+
name = text.strip
|
75
|
+
end
|
76
|
+
@stream.consume
|
77
|
+
else
|
78
|
+
name = ""
|
79
|
+
end
|
80
|
+
|
81
|
+
cookware_item = Cookware.new(name: name, quantity: 1)
|
82
|
+
[cookware_item, remaining_text]
|
83
|
+
end
|
84
|
+
|
85
|
+
def extract_name_until_brace(brace_index)
|
86
|
+
name_parts = []
|
87
|
+
|
88
|
+
while @stream.position < brace_index && !@stream.eof?
|
89
|
+
case @stream.current.type
|
90
|
+
when :text, :hyphen
|
91
|
+
name_parts << @stream.current.value
|
92
|
+
end
|
93
|
+
@stream.consume
|
94
|
+
end
|
95
|
+
|
96
|
+
name_parts.join.strip
|
97
|
+
end
|
98
|
+
|
99
|
+
def extract_quantity
|
100
|
+
text_parts = []
|
101
|
+
|
102
|
+
while !@stream.eof? && @stream.current.type != :close_brace
|
103
|
+
case @stream.current.type
|
104
|
+
when :percent
|
105
|
+
# Skip percent in cookware - quantity comes after
|
106
|
+
@stream.consume
|
107
|
+
when :text
|
108
|
+
text_parts << @stream.current.value
|
109
|
+
@stream.consume
|
110
|
+
else
|
111
|
+
@stream.consume
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
return nil if text_parts.empty?
|
116
|
+
|
117
|
+
combined_text = text_parts.join.strip
|
118
|
+
parse_quantity_value(combined_text)
|
119
|
+
end
|
120
|
+
|
121
|
+
def parse_quantity_value(text)
|
122
|
+
case text
|
123
|
+
when /^\d+$/
|
124
|
+
text.to_i
|
125
|
+
when /^\d+\.\d+$/
|
126
|
+
text.to_f
|
127
|
+
else
|
128
|
+
text
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|