cooklang 1.0.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 +340 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +84 -0
- data/README.md +10 -5
- data/Rakefile +12 -0
- 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/lexer.rb +5 -14
- data/lib/cooklang/parser.rb +24 -653
- 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 +25 -15
- data/lib/cooklang/step.rb +12 -2
- data/lib/cooklang/timer.rb +3 -1
- data/lib/cooklang/token_stream.rb +130 -0
- data/lib/cooklang/version.rb +1 -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 +162 -6
@@ -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
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../ingredient"
|
4
|
+
require_relative "../token_stream"
|
5
|
+
|
6
|
+
module Cooklang
|
7
|
+
module Parsers
|
8
|
+
class IngredientParser
|
9
|
+
def initialize(stream)
|
10
|
+
@stream = stream
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse
|
14
|
+
return nil unless @stream.current&.type == :ingredient_marker
|
15
|
+
@stream.consume(:ingredient_marker) # Skip the @ marker
|
16
|
+
|
17
|
+
return nil if invalid_syntax?
|
18
|
+
|
19
|
+
brace_index = find_next_brace
|
20
|
+
|
21
|
+
if brace_index
|
22
|
+
parse_braced_ingredient(brace_index)
|
23
|
+
else
|
24
|
+
parse_simple_ingredient
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def invalid_syntax?
|
30
|
+
@stream.current&.type == :text && @stream.current.value.start_with?(" ")
|
31
|
+
end
|
32
|
+
|
33
|
+
def find_next_brace
|
34
|
+
@stream.find_next(:open_brace)
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse_braced_ingredient(brace_index)
|
38
|
+
name = extract_name_until_brace(brace_index)
|
39
|
+
@stream.consume(:open_brace) # Skip open brace
|
40
|
+
|
41
|
+
quantity, unit = extract_quantity_and_unit
|
42
|
+
@stream.consume(:close_brace) # Skip close brace
|
43
|
+
|
44
|
+
notes = extract_notes
|
45
|
+
|
46
|
+
# Default values according to original logic
|
47
|
+
quantity = "some" if (quantity.nil? || quantity == "") && (unit.nil? || unit == "")
|
48
|
+
unit = nil if unit == ""
|
49
|
+
|
50
|
+
Ingredient.new(name: name, quantity: quantity, unit: unit, notes: notes)
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse_simple_ingredient
|
54
|
+
remaining_text = nil
|
55
|
+
|
56
|
+
if @stream.current&.type == :text
|
57
|
+
text = @stream.current.value
|
58
|
+
if text.match(/^([a-zA-Z0-9_]+)(.*)$/)
|
59
|
+
name = ::Regexp.last_match(1)
|
60
|
+
remaining_text = ::Regexp.last_match(2)
|
61
|
+
else
|
62
|
+
name = text.strip
|
63
|
+
end
|
64
|
+
@stream.consume
|
65
|
+
else
|
66
|
+
name = ""
|
67
|
+
end
|
68
|
+
|
69
|
+
ingredient = Ingredient.new(name: name, quantity: "some", unit: nil, notes: nil)
|
70
|
+
[ingredient, remaining_text]
|
71
|
+
end
|
72
|
+
|
73
|
+
def extract_name_until_brace(brace_index)
|
74
|
+
name_parts = []
|
75
|
+
@stream.position
|
76
|
+
|
77
|
+
while @stream.position < brace_index && !@stream.eof?
|
78
|
+
case @stream.current.type
|
79
|
+
when :text, :hyphen
|
80
|
+
name_parts << @stream.current.value
|
81
|
+
end
|
82
|
+
@stream.consume
|
83
|
+
end
|
84
|
+
|
85
|
+
name_parts.join.strip
|
86
|
+
end
|
87
|
+
|
88
|
+
def extract_quantity_and_unit
|
89
|
+
quantity = nil
|
90
|
+
unit = nil
|
91
|
+
text_parts = []
|
92
|
+
|
93
|
+
while !@stream.eof? && @stream.current.type != :close_brace
|
94
|
+
case @stream.current.type
|
95
|
+
when :percent
|
96
|
+
if !text_parts.empty?
|
97
|
+
quantity_text = text_parts.join.strip
|
98
|
+
quantity = parse_quantity_value(quantity_text)
|
99
|
+
end
|
100
|
+
text_parts = []
|
101
|
+
@stream.consume
|
102
|
+
when :text
|
103
|
+
text_parts << @stream.current.value
|
104
|
+
@stream.consume
|
105
|
+
else
|
106
|
+
@stream.consume
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
if !text_parts.empty?
|
111
|
+
combined_text = text_parts.join.strip
|
112
|
+
if quantity.nil?
|
113
|
+
# Try to parse as numeric quantity first, fallback to string if not purely numeric
|
114
|
+
if combined_text.match?(/^\d+$/)
|
115
|
+
quantity = combined_text.to_i
|
116
|
+
unit = ""
|
117
|
+
elsif combined_text.match?(/^\d+\.\d+$/)
|
118
|
+
quantity = combined_text.to_f
|
119
|
+
unit = ""
|
120
|
+
else
|
121
|
+
# Non-numeric content, keep as string quantity
|
122
|
+
quantity = combined_text
|
123
|
+
unit = ""
|
124
|
+
end
|
125
|
+
else
|
126
|
+
unit = combined_text
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
[quantity, unit]
|
131
|
+
end
|
132
|
+
|
133
|
+
def parse_quantity_value(text)
|
134
|
+
case text
|
135
|
+
when /^\d+$/
|
136
|
+
text.to_i
|
137
|
+
when /^\d+\.\d+$/
|
138
|
+
text.to_f
|
139
|
+
else
|
140
|
+
text
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def extract_quantity_from_text(text)
|
145
|
+
# Simple quantity extraction - this could use QuantityExtractor if needed
|
146
|
+
if text.match(/^(\d+(?:\.\d+)?)\s*(.*)$/)
|
147
|
+
quantity_val = ::Regexp.last_match(1)
|
148
|
+
rest = ::Regexp.last_match(2)
|
149
|
+
|
150
|
+
quantity = quantity_val.include?(".") ? quantity_val.to_f : quantity_val.to_i
|
151
|
+
unit = rest.empty? ? nil : rest
|
152
|
+
|
153
|
+
[quantity, unit]
|
154
|
+
else
|
155
|
+
[text, nil]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def extract_notes
|
160
|
+
return nil unless @stream.current&.type == :open_paren
|
161
|
+
|
162
|
+
@stream.consume(:open_paren) # Skip open paren
|
163
|
+
notes_parts = []
|
164
|
+
|
165
|
+
while !@stream.eof? && @stream.current.type != :close_paren
|
166
|
+
if @stream.current.type == :text
|
167
|
+
notes_parts << @stream.current.value
|
168
|
+
end
|
169
|
+
@stream.consume
|
170
|
+
end
|
171
|
+
|
172
|
+
@stream.consume(:close_paren) if @stream.current&.type == :close_paren
|
173
|
+
|
174
|
+
notes = notes_parts.join.strip
|
175
|
+
notes.empty? ? nil : notes
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../timer"
|
4
|
+
require_relative "../token_stream"
|
5
|
+
|
6
|
+
module Cooklang
|
7
|
+
module Parsers
|
8
|
+
class TimerParser
|
9
|
+
def initialize(stream)
|
10
|
+
@stream = stream
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse
|
14
|
+
return nil unless @stream.current&.type == :timer_marker
|
15
|
+
@stream.consume(:timer_marker) # Skip the ~ marker
|
16
|
+
|
17
|
+
return nil if invalid_syntax?
|
18
|
+
|
19
|
+
brace_index = find_next_brace
|
20
|
+
|
21
|
+
if brace_index
|
22
|
+
parse_named_timer(brace_index)
|
23
|
+
else
|
24
|
+
parse_simple_timer
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def invalid_syntax?
|
30
|
+
@stream.current&.type == :text && @stream.current.value.start_with?(" ")
|
31
|
+
end
|
32
|
+
|
33
|
+
def find_next_brace
|
34
|
+
@stream.find_next(:open_brace)
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse_named_timer(brace_index)
|
38
|
+
name = nil
|
39
|
+
|
40
|
+
# Extract name if there's text before the brace
|
41
|
+
if @stream.position < brace_index && @stream.current&.type == :text
|
42
|
+
name = @stream.current.value.strip
|
43
|
+
@stream.advance_to(brace_index)
|
44
|
+
end
|
45
|
+
|
46
|
+
@stream.consume(:open_brace) # Skip open brace
|
47
|
+
|
48
|
+
duration, unit = extract_duration
|
49
|
+
@stream.consume(:close_brace) # Skip close brace
|
50
|
+
|
51
|
+
Timer.new(name: name, duration: duration, unit: unit)
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse_simple_timer
|
55
|
+
remaining_text = nil
|
56
|
+
name = nil
|
57
|
+
|
58
|
+
if @stream.current&.type == :text
|
59
|
+
text = @stream.current.value
|
60
|
+
if text.match(/^([a-zA-Z0-9_]+)(.*)$/)
|
61
|
+
name = ::Regexp.last_match(1)
|
62
|
+
remaining_text = ::Regexp.last_match(2)
|
63
|
+
else
|
64
|
+
name = text.strip
|
65
|
+
end
|
66
|
+
@stream.consume
|
67
|
+
end
|
68
|
+
|
69
|
+
timer = Timer.new(name: name, duration: nil, unit: nil)
|
70
|
+
[timer, remaining_text]
|
71
|
+
end
|
72
|
+
|
73
|
+
def extract_duration
|
74
|
+
duration_parts = []
|
75
|
+
unit = nil
|
76
|
+
|
77
|
+
while !@stream.eof? && @stream.current.type != :close_brace
|
78
|
+
case @stream.current.type
|
79
|
+
when :text, :hyphen
|
80
|
+
duration_parts << @stream.current.value
|
81
|
+
@stream.consume
|
82
|
+
when :percent
|
83
|
+
@stream.consume
|
84
|
+
# Unit comes after percent
|
85
|
+
if @stream.current&.type == :text
|
86
|
+
unit = @stream.current.value
|
87
|
+
@stream.consume
|
88
|
+
end
|
89
|
+
else
|
90
|
+
@stream.consume
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
return [nil, nil] if duration_parts.empty?
|
95
|
+
|
96
|
+
duration_text = duration_parts.join
|
97
|
+
|
98
|
+
if unit
|
99
|
+
duration = parse_duration_value(duration_text)
|
100
|
+
[duration, unit]
|
101
|
+
else
|
102
|
+
parse_duration_with_unit(duration_text)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def parse_duration_value(text)
|
107
|
+
case text
|
108
|
+
when /^\d+$/
|
109
|
+
text.to_i
|
110
|
+
when /^\d+\.\d+$/
|
111
|
+
text.to_f
|
112
|
+
else
|
113
|
+
text # Keep ranges like "2-3"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def parse_duration_with_unit(text)
|
118
|
+
# Simple duration parsing - could be enhanced with DurationExtractor
|
119
|
+
# First check if it's just a number (integer or decimal)
|
120
|
+
if text.match?(/^(\d+(?:\.\d+)?)$/)
|
121
|
+
parsed_duration = text.include?(".") ? text.to_f : text.to_i
|
122
|
+
[parsed_duration, nil]
|
123
|
+
elsif text.match(/^(\d+(?:\.\d+)?)\s*([a-zA-Z]+)$/)
|
124
|
+
duration = ::Regexp.last_match(1)
|
125
|
+
unit = ::Regexp.last_match(2)
|
126
|
+
|
127
|
+
parsed_duration = duration.include?(".") ? duration.to_f : duration.to_i
|
128
|
+
[parsed_duration, unit]
|
129
|
+
else
|
130
|
+
[text, nil]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../token_stream"
|
4
|
+
require_relative "../parsers/ingredient_parser"
|
5
|
+
require_relative "../parsers/cookware_parser"
|
6
|
+
require_relative "../parsers/timer_parser"
|
7
|
+
|
8
|
+
module Cooklang
|
9
|
+
module Processors
|
10
|
+
class ElementParser
|
11
|
+
class << self
|
12
|
+
def parse_ingredient(tokens, start_index)
|
13
|
+
parse_with_parser(tokens, start_index, Parsers::IngredientParser)
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse_cookware(tokens, start_index)
|
17
|
+
parse_with_parser(tokens, start_index, Parsers::CookwareParser)
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_timer(tokens, start_index)
|
21
|
+
parse_with_parser(tokens, start_index, Parsers::TimerParser)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def parse_with_parser(tokens, start_index, parser_class)
|
26
|
+
stream = TokenStream.new(tokens)
|
27
|
+
stream.advance_to(start_index)
|
28
|
+
|
29
|
+
parser = parser_class.new(stream)
|
30
|
+
result = parser.parse
|
31
|
+
consumed = stream.position - start_index
|
32
|
+
|
33
|
+
# Handle the return format
|
34
|
+
if result.is_a?(Array)
|
35
|
+
[result[0], consumed, result[1]]
|
36
|
+
elsif result
|
37
|
+
[result, consumed, nil]
|
38
|
+
else
|
39
|
+
[nil, 1, nil]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../metadata"
|
4
|
+
|
5
|
+
module Cooklang
|
6
|
+
module Processors
|
7
|
+
class MetadataProcessor
|
8
|
+
class << self
|
9
|
+
def extract_metadata(tokens)
|
10
|
+
metadata = Metadata.new
|
11
|
+
content_tokens = []
|
12
|
+
0
|
13
|
+
|
14
|
+
# Check for YAML front matter
|
15
|
+
if tokens.first&.type == :yaml_delimiter
|
16
|
+
metadata, content_tokens, _ = extract_yaml_frontmatter(tokens)
|
17
|
+
else
|
18
|
+
content_tokens = tokens.dup
|
19
|
+
end
|
20
|
+
|
21
|
+
# Extract inline metadata
|
22
|
+
extract_inline_metadata(content_tokens, metadata)
|
23
|
+
|
24
|
+
[metadata, content_tokens]
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def extract_yaml_frontmatter(tokens)
|
29
|
+
metadata = Metadata.new
|
30
|
+
i = 1 # Skip the first ---
|
31
|
+
yaml_content = []
|
32
|
+
|
33
|
+
# Find the closing --- delimiter
|
34
|
+
while i < tokens.length
|
35
|
+
if tokens[i].type == :yaml_delimiter
|
36
|
+
# Found closing delimiter
|
37
|
+
break
|
38
|
+
elsif tokens[i].type == :text
|
39
|
+
yaml_content << tokens[i].value
|
40
|
+
elsif tokens[i].type == :newline
|
41
|
+
yaml_content << "\n"
|
42
|
+
elsif tokens[i].type == :metadata_marker
|
43
|
+
yaml_content << ">>"
|
44
|
+
end
|
45
|
+
i += 1
|
46
|
+
end
|
47
|
+
|
48
|
+
# Skip the closing --- if we found it
|
49
|
+
i += 1 if i < tokens.length && tokens[i].type == :yaml_delimiter
|
50
|
+
|
51
|
+
# Parse YAML content if we have any
|
52
|
+
if yaml_content.any?
|
53
|
+
yaml_text = yaml_content.join.strip
|
54
|
+
parse_yaml_content(yaml_text, metadata) unless yaml_text.empty?
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return remaining tokens
|
58
|
+
remaining_tokens = tokens[i..]
|
59
|
+
|
60
|
+
[metadata, remaining_tokens, i]
|
61
|
+
end
|
62
|
+
|
63
|
+
def parse_yaml_content(yaml_text, metadata)
|
64
|
+
# Simple YAML parsing - split by lines and parse key-value pairs
|
65
|
+
yaml_text.split("\n").each do |line|
|
66
|
+
line = line.strip
|
67
|
+
next if line.empty? || line.start_with?("#")
|
68
|
+
|
69
|
+
if line.match(/^([^:]+):\s*(.*)$/)
|
70
|
+
key = ::Regexp.last_match(1).strip
|
71
|
+
value = ::Regexp.last_match(2).strip
|
72
|
+
|
73
|
+
# Remove quotes if present
|
74
|
+
value = value.gsub(/^["']|["']$/, "") if value.match?(/^["'].*["']$/)
|
75
|
+
|
76
|
+
# Parse numeric values
|
77
|
+
parsed_value = parse_metadata_value(value)
|
78
|
+
metadata[key] = parsed_value unless value.empty?
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def extract_inline_metadata(tokens, metadata)
|
84
|
+
tokens_to_remove = []
|
85
|
+
|
86
|
+
tokens.each_with_index do |token, index|
|
87
|
+
next unless token.type == :metadata_marker
|
88
|
+
|
89
|
+
# Look for metadata pattern: >> key: value
|
90
|
+
if index + 1 < tokens.length && tokens[index + 1].type == :text
|
91
|
+
text = tokens[index + 1].value.strip
|
92
|
+
|
93
|
+
if text.match(/^([^:]+):\s*(.+)$/)
|
94
|
+
key = ::Regexp.last_match(1).strip
|
95
|
+
value = ::Regexp.last_match(2).strip
|
96
|
+
|
97
|
+
# Remove quotes if present
|
98
|
+
value = value.gsub(/^["']|["']$/, "") if value.match?(/^["'].*["']$/)
|
99
|
+
|
100
|
+
# Parse numeric values
|
101
|
+
parsed_value = parse_metadata_value(value)
|
102
|
+
metadata[key] = parsed_value
|
103
|
+
|
104
|
+
# Mark both marker and text tokens for removal
|
105
|
+
tokens_to_remove << index << (index + 1)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Remove only the specific metadata tokens that were processed
|
111
|
+
tokens.reject!.with_index { |token, index| tokens_to_remove.include?(index) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def parse_metadata_value(value)
|
115
|
+
return value if value.empty?
|
116
|
+
|
117
|
+
# Try to parse as integer
|
118
|
+
return value.to_i if value.match?(/^\d+$/)
|
119
|
+
|
120
|
+
# Try to parse as float
|
121
|
+
return value.to_f if value.match?(/^\d+\.\d+$/)
|
122
|
+
|
123
|
+
# Return as string
|
124
|
+
value
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|