prompter-ruby 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 62d43dac53307b79d18f84fcf1c40fd88dbf19a5c337b9e914c13fa010b73dbe
4
+ data.tar.gz: 7e2c23ca40c7178b0821bf105bd4389a33b2f293ca4ae76551b762262b17e9d2
5
+ SHA512:
6
+ metadata.gz: 630c8c261d232440ac6335e0d479a8933b9e2c8f57379d63a5a94da0af59aabc9cabd1c32e549b07663c14902d8f70dcf3d9cc211832900a6b6f81b61f4e3ff6
7
+ data.tar.gz: 170c646d22cd2e94a1f63772f1772660acd4f33e9cc0215ba5432d29766df0560c9be3114b904cdf1b3c8116fe30226adacb7318a8f95cf749b4265a7da71f89
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-03-09)
4
+
5
+ - Initial release
6
+ - Template parser with variables, conditionals, and loops
7
+ - Dot notation for nested variables
8
+ - If/else/endif blocks with truthiness evaluation
9
+ - For/endfor loops over collections
10
+ - Negation support
11
+ - File-based prompt library with versioning
12
+ - Template caching
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Johannes Dwi Cahyo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # prompter-ruby
2
+
3
+ Prompt template engine for Ruby with variables, conditionals, loops, and versioning.
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ gem "prompter-ruby", "~> 0.1"
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ruby
14
+ require "prompter_ruby"
15
+
16
+ result = PrompterRuby.render("Hello {{name}}!", name: "World")
17
+
18
+ template = PrompterRuby::Template.new(<<~PROMPT)
19
+ You are {{bot_name}}.
20
+ {% if context %}Context: {{context}}{% endif %}
21
+ {% for rule in rules %}- {{rule}}
22
+ {% endfor %}
23
+ PROMPT
24
+
25
+ result = template.render(
26
+ bot_name: "Max",
27
+ context: "Some info",
28
+ rules: ["Be concise", "Be helpful"]
29
+ )
30
+
31
+ library = PrompterRuby::Library.new("./prompts/")
32
+ template = library.get("rag_answer", version: "v2")
33
+ ```
34
+
35
+ ## License
36
+
37
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake/testtask"
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << "test"
7
+ t.libs << "lib"
8
+ t.test_files = FileList["test/**/test_*.rb"]
9
+ end
10
+
11
+ task default: :test
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrompterRuby
4
+ class Error < StandardError; end
5
+ class ParseError < Error; end
6
+ class RenderError < Error; end
7
+ class UndefinedVariableError < RenderError; end
8
+ class TemplateNotFoundError < Error; end
9
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrompterRuby
4
+ class Library
5
+ def initialize(path)
6
+ @path = path
7
+ @cache = {}
8
+ end
9
+
10
+ def get(name, version: nil)
11
+ cache_key = "#{name}:#{version || "latest"}"
12
+ @cache[cache_key] ||= load_template(name, version: version)
13
+ end
14
+
15
+ def list
16
+ Dir.glob(File.join(@path, "**/*.{txt,prompt,md}")).map do |f|
17
+ relative = f.sub("#{@path}/", "")
18
+ relative.sub(/\.[^.]+$/, "")
19
+ end.sort
20
+ end
21
+
22
+ def clear_cache
23
+ @cache = {}
24
+ end
25
+
26
+ private
27
+
28
+ def load_template(name, version: nil)
29
+ file_path = find_template_file(name, version: version)
30
+ raise TemplateNotFoundError, "Template not found: #{name}" unless file_path
31
+
32
+ source = File.read(file_path)
33
+ Template.new(source)
34
+ end
35
+
36
+ def find_template_file(name, version: nil)
37
+ extensions = %w[.txt .prompt .md]
38
+
39
+ if version
40
+ extensions.each do |ext|
41
+ path = File.join(@path, "#{name}.#{version}#{ext}")
42
+ return path if File.exist?(path)
43
+ end
44
+
45
+ extensions.each do |ext|
46
+ path = File.join(@path, name, "#{version}#{ext}")
47
+ return path if File.exist?(path)
48
+ end
49
+ end
50
+
51
+ extensions.each do |ext|
52
+ path = File.join(@path, "#{name}#{ext}")
53
+ return path if File.exist?(path)
54
+ end
55
+
56
+ nil
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrompterRuby
4
+ module Nodes
5
+ class ForNode
6
+ def initialize(item_name, collection_name, body_nodes)
7
+ @item_name = item_name.strip
8
+ @collection_name = collection_name.strip
9
+ @body_nodes = body_nodes
10
+ end
11
+
12
+ def render(context)
13
+ collection = resolve_collection(context)
14
+ return "" unless collection.is_a?(Enumerable)
15
+
16
+ collection.map do |item|
17
+ loop_context = context.merge(@item_name.to_sym => item)
18
+ @body_nodes.map { |n| n.render(loop_context) }.join
19
+ end.join
20
+ end
21
+
22
+ private
23
+
24
+ def resolve_collection(context)
25
+ parts = @collection_name.split(".")
26
+ value = context
27
+
28
+ parts.each do |part|
29
+ if value.is_a?(Hash)
30
+ key = value.key?(part.to_sym) ? part.to_sym : part
31
+ value = value[key]
32
+ else
33
+ return []
34
+ end
35
+ end
36
+
37
+ value
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrompterRuby
4
+ module Nodes
5
+ class IfNode
6
+ def initialize(condition, true_nodes, false_nodes = [])
7
+ @condition = condition.strip
8
+ @true_nodes = true_nodes
9
+ @false_nodes = false_nodes
10
+ end
11
+
12
+ def render(context)
13
+ if evaluate_condition(context)
14
+ @true_nodes.map { |n| n.render(context) }.join
15
+ else
16
+ @false_nodes.map { |n| n.render(context) }.join
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def evaluate_condition(context)
23
+ negate = false
24
+ condition = @condition
25
+
26
+ if condition.start_with?("not ")
27
+ negate = true
28
+ condition = condition.sub("not ", "").strip
29
+ end
30
+
31
+ value = resolve_value(condition, context)
32
+ result = truthy?(value)
33
+ negate ? !result : result
34
+ end
35
+
36
+ def resolve_value(name, context)
37
+ parts = name.split(".")
38
+ value = context
39
+
40
+ parts.each do |part|
41
+ if value.is_a?(Hash)
42
+ key = value.key?(part.to_sym) ? part.to_sym : part
43
+ value = value[key]
44
+ elsif value.respond_to?(part)
45
+ value = value.send(part)
46
+ else
47
+ return nil
48
+ end
49
+ end
50
+
51
+ value
52
+ end
53
+
54
+ def truthy?(value)
55
+ return false if value.nil?
56
+ return false if value == false
57
+ return false if value.respond_to?(:empty?) && value.empty?
58
+
59
+ true
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrompterRuby
4
+ module Nodes
5
+ class TextNode
6
+ attr_reader :text
7
+
8
+ def initialize(text)
9
+ @text = text
10
+ end
11
+
12
+ def render(_context)
13
+ @text
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrompterRuby
4
+ module Nodes
5
+ class VariableNode
6
+ attr_reader :name
7
+
8
+ def initialize(name)
9
+ @name = name.strip
10
+ end
11
+
12
+ def render(context)
13
+ value = resolve(context)
14
+
15
+ if value.nil? && context[:__strict__]
16
+ raise UndefinedVariableError, "Undefined variable: '#{@name}'"
17
+ end
18
+
19
+ value.to_s
20
+ end
21
+
22
+ private
23
+
24
+ def resolve(context)
25
+ parts = @name.split(".")
26
+ value = context
27
+
28
+ parts.each do |part|
29
+ next if part == "__strict__"
30
+
31
+ if value.is_a?(Hash)
32
+ key = value.key?(part.to_sym) ? part.to_sym : part
33
+ if value.key?(key)
34
+ value = value[key]
35
+ else
36
+ return nil
37
+ end
38
+ elsif value.respond_to?(part)
39
+ value = value.send(part)
40
+ else
41
+ return nil
42
+ end
43
+ end
44
+
45
+ value
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrompterRuby
4
+ class Parser
5
+ VARIABLE_PATTERN = /\{\{(.*?)\}\}/
6
+ TAG_PATTERN = /\{%(.*?)%\}/
7
+
8
+ def parse(source)
9
+ tokens = tokenize(source)
10
+ tokens = strip_tag_whitespace(tokens)
11
+ parse_nodes(tokens, stop_tags: [], open_tag: nil, open_line: nil)
12
+ end
13
+
14
+ private
15
+
16
+ def tokenize(source)
17
+ tokens = []
18
+ scanner = source
19
+ offset = 0
20
+
21
+ while scanner.length > 0
22
+ var_match = scanner.match(VARIABLE_PATTERN)
23
+ tag_match = scanner.match(TAG_PATTERN)
24
+
25
+ next_match = nil
26
+ next_pos = scanner.length
27
+
28
+ if var_match && var_match.begin(0) < next_pos
29
+ next_match = :variable
30
+ next_pos = var_match.begin(0)
31
+ end
32
+
33
+ if tag_match && tag_match.begin(0) < next_pos
34
+ next_match = :tag
35
+ next_pos = tag_match.begin(0)
36
+ end
37
+
38
+ if next_pos > 0
39
+ tokens << [:text, scanner[0...next_pos], line_number(source, offset)]
40
+ end
41
+
42
+ case next_match
43
+ when :variable
44
+ tokens << [:variable, var_match[1], line_number(source, offset + var_match.begin(0))]
45
+ offset += var_match.end(0)
46
+ scanner = scanner[var_match.end(0)..]
47
+ when :tag
48
+ tokens << [:tag, tag_match[1].strip, line_number(source, offset + tag_match.begin(0))]
49
+ offset += tag_match.end(0)
50
+ scanner = scanner[tag_match.end(0)..]
51
+ else
52
+ break
53
+ end
54
+ end
55
+
56
+ tokens
57
+ end
58
+
59
+ def line_number(source, offset)
60
+ source[0...offset].count("\n") + 1
61
+ end
62
+
63
+ # Strip whitespace around tag-only lines.
64
+ # If a tag sits alone on a line (only whitespace before/after it on that line),
65
+ # remove that whitespace and the trailing newline so the tag line vanishes.
66
+ def strip_tag_whitespace(tokens)
67
+ result = []
68
+
69
+ tokens.each_with_index do |token, i|
70
+ if token[0] == :tag
71
+ # Check if preceding text ends with only whitespace since last newline
72
+ # and following text starts with only a newline (possibly preceded by whitespace)
73
+ prev_idx = result.length - 1
74
+ next_idx = i + 1
75
+
76
+ prev_is_blank_tail = false
77
+ next_is_blank_head = false
78
+
79
+ # Check text before tag: ends with \n followed by only spaces/tabs (or is start of string with only spaces/tabs)
80
+ if prev_idx >= 0 && result[prev_idx][0] == :text
81
+ text = result[prev_idx][1]
82
+ prev_is_blank_tail = text.match?(/(?:\A|(?<=\n))[ \t]*\z/)
83
+ elsif prev_idx < 0
84
+ # Tag is at the very start
85
+ prev_is_blank_tail = true
86
+ end
87
+
88
+ # Check text after tag: starts with optional spaces/tabs then \n (or is end of string)
89
+ if next_idx < tokens.length && tokens[next_idx][0] == :text
90
+ text = tokens[next_idx][1]
91
+ next_is_blank_head = text.match?(/\A[ \t]*\n/) || text.match?(/\A[ \t]*\z/)
92
+ elsif next_idx >= tokens.length
93
+ # Tag is at the very end
94
+ next_is_blank_head = true
95
+ end
96
+
97
+ if prev_is_blank_tail && next_is_blank_head
98
+ # Trim trailing whitespace from previous text node (after last newline)
99
+ if prev_idx >= 0 && result[prev_idx][0] == :text
100
+ text = result[prev_idx][1]
101
+ trimmed = text.sub(/(?:(?<=\n)|(?:\A))[ \t]*\z/, "")
102
+ if trimmed.empty?
103
+ result.pop
104
+ else
105
+ result[prev_idx] = [:text, trimmed, result[prev_idx][2]]
106
+ end
107
+ end
108
+
109
+ result << token
110
+
111
+ # Mark next text token for trimming (leading whitespace + first newline)
112
+ if next_idx < tokens.length && tokens[next_idx][0] == :text
113
+ text = tokens[next_idx][1]
114
+ trimmed = text.sub(/\A[ \t]*\n/, "")
115
+ if trimmed.empty?
116
+ # Skip this text token entirely - will be handled by replacing in tokens
117
+ tokens[next_idx] = [:text_skip, "", tokens[next_idx][2]]
118
+ else
119
+ tokens[next_idx] = [:text, trimmed, tokens[next_idx][2]]
120
+ end
121
+ end
122
+ else
123
+ result << token
124
+ end
125
+ elsif token[0] == :text_skip
126
+ # Skip tokens marked for removal
127
+ next
128
+ else
129
+ result << token
130
+ end
131
+ end
132
+
133
+ result
134
+ end
135
+
136
+ def parse_nodes(tokens, stop_tags:, open_tag:, open_line:)
137
+ nodes = []
138
+ index = 0
139
+
140
+ while index < tokens.length
141
+ type, value, line = tokens[index]
142
+
143
+ case type
144
+ when :text
145
+ nodes << Nodes::TextNode.new(value)
146
+ index += 1
147
+ when :variable
148
+ nodes << Nodes::VariableNode.new(value)
149
+ index += 1
150
+ when :tag
151
+ if stop_tags.any? { |tag| value.start_with?(tag) }
152
+ return [nodes, index]
153
+ end
154
+
155
+ if value.start_with?("if ")
156
+ condition = value.sub("if ", "").strip
157
+ result = parse_nodes(tokens[(index + 1)..], stop_tags: ["else", "elsif", "endif"], open_tag: "if", open_line: line)
158
+
159
+ if result.is_a?(Array) && result.length == 2
160
+ true_nodes, end_index = result
161
+ else
162
+ raise ParseError, "Unclosed 'if' block starting at line #{line}"
163
+ end
164
+
165
+ index = index + 1 + end_index
166
+
167
+ false_nodes = []
168
+ if tokens[index] && tokens[index][1]&.start_with?("else")
169
+ else_line = tokens[index][2]
170
+ result = parse_nodes(tokens[(index + 1)..], stop_tags: ["endif"], open_tag: "else", open_line: else_line)
171
+
172
+ if result.is_a?(Array) && result.length == 2
173
+ false_nodes, end_index = result
174
+ else
175
+ raise ParseError, "Unclosed 'else' block starting at line #{else_line}"
176
+ end
177
+
178
+ index = index + 1 + end_index
179
+ end
180
+
181
+ # Verify we actually landed on endif
182
+ unless tokens[index] && tokens[index][1]&.start_with?("endif")
183
+ raise ParseError, "Unclosed 'if' block starting at line #{line}"
184
+ end
185
+
186
+ index += 1
187
+
188
+ nodes << Nodes::IfNode.new(condition, true_nodes, false_nodes)
189
+ elsif value.start_with?("for ")
190
+ match = value.match(/for\s+(\w+)\s+in\s+(\S+)/)
191
+ raise ParseError, "Invalid for syntax: #{value}" unless match
192
+
193
+ item_name = match[1]
194
+ collection_name = match[2]
195
+
196
+ result = parse_nodes(tokens[(index + 1)..], stop_tags: ["endfor"], open_tag: "for", open_line: line)
197
+
198
+ if result.is_a?(Array) && result.length == 2
199
+ body_nodes, end_index = result
200
+ else
201
+ raise ParseError, "Unclosed 'for' block starting at line #{line}"
202
+ end
203
+
204
+ # Verify we actually landed on endfor
205
+ unless tokens[index + 1 + end_index] && tokens[index + 1 + end_index][1]&.start_with?("endfor")
206
+ raise ParseError, "Unclosed 'for' block starting at line #{line}"
207
+ end
208
+
209
+ index = index + 1 + end_index + 1
210
+
211
+ nodes << Nodes::ForNode.new(item_name, collection_name, body_nodes)
212
+ else
213
+ nodes << Nodes::TextNode.new("{%#{value}%}")
214
+ index += 1
215
+ end
216
+ end
217
+ end
218
+
219
+ if stop_tags.any?
220
+ raise ParseError, "Unclosed '#{open_tag}' block starting at line #{open_line}"
221
+ end
222
+
223
+ nodes
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrompterRuby
4
+ class Template
5
+ attr_reader :source
6
+
7
+ def initialize(source)
8
+ raise ArgumentError, "Template source cannot be nil" if source.nil?
9
+
10
+ @source = source
11
+ @parser = Parser.new
12
+ @nodes = @parser.parse(source)
13
+ end
14
+
15
+ def render(variables = {})
16
+ raise ArgumentError, "Variables must be a Hash" unless variables.is_a?(Hash)
17
+
18
+ context = normalize_keys(variables)
19
+
20
+ if context.delete(:strict)
21
+ context[:__strict__] = true
22
+ end
23
+
24
+ @nodes.map { |node| node.render(context) }.join
25
+ end
26
+
27
+ def valid?
28
+ validate!
29
+ true
30
+ rescue ParseError
31
+ false
32
+ end
33
+
34
+ def validate!
35
+ @parser.parse(@source)
36
+ true
37
+ end
38
+
39
+ private
40
+
41
+ def normalize_keys(hash)
42
+ hash.transform_keys(&:to_sym)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrompterRuby
4
+ VERSION = "0.1.1"
5
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "prompter_ruby/version"
4
+ require_relative "prompter_ruby/errors"
5
+ require_relative "prompter_ruby/nodes/text_node"
6
+ require_relative "prompter_ruby/nodes/variable_node"
7
+ require_relative "prompter_ruby/nodes/if_node"
8
+ require_relative "prompter_ruby/nodes/for_node"
9
+ require_relative "prompter_ruby/parser"
10
+ require_relative "prompter_ruby/template"
11
+ require_relative "prompter_ruby/library"
12
+
13
+ module PrompterRuby
14
+ class << self
15
+ def render(source, variables = {})
16
+ Template.new(source).render(variables)
17
+ end
18
+
19
+ def library(path)
20
+ Library.new(path)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/prompter_ruby/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "prompter-ruby"
7
+ spec.version = PrompterRuby::VERSION
8
+ spec.authors = ["Johannes Dwi Cahyo"]
9
+ spec.email = ["johannes@example.com"]
10
+ spec.summary = "Prompt template engine for Ruby"
11
+ spec.description = "Shared prompt template engine with variables, conditionals, loops, and prompt versioning. Replaces ad-hoc templating across LLM gems."
12
+ spec.homepage = "https://github.com/johannesdwicahyo/prompter-ruby"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 3.0.0"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
19
+
20
+ spec.files = Dir[
21
+ "lib/**/*.rb",
22
+ "README.md",
23
+ "LICENSE",
24
+ "CHANGELOG.md",
25
+ "Rakefile",
26
+ "prompter-ruby.gemspec"
27
+ ]
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "minitest", "~> 5.0"
31
+ spec.add_development_dependency "rake", "~> 13.0"
32
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prompter-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Johannes Dwi Cahyo
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ description: Shared prompt template engine with variables, conditionals, loops, and
41
+ prompt versioning. Replaces ad-hoc templating across LLM gems.
42
+ email:
43
+ - johannes@example.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - LICENSE
50
+ - README.md
51
+ - Rakefile
52
+ - lib/prompter_ruby.rb
53
+ - lib/prompter_ruby/errors.rb
54
+ - lib/prompter_ruby/library.rb
55
+ - lib/prompter_ruby/nodes/for_node.rb
56
+ - lib/prompter_ruby/nodes/if_node.rb
57
+ - lib/prompter_ruby/nodes/text_node.rb
58
+ - lib/prompter_ruby/nodes/variable_node.rb
59
+ - lib/prompter_ruby/parser.rb
60
+ - lib/prompter_ruby/template.rb
61
+ - lib/prompter_ruby/version.rb
62
+ - prompter-ruby.gemspec
63
+ homepage: https://github.com/johannesdwicahyo/prompter-ruby
64
+ licenses:
65
+ - MIT
66
+ metadata:
67
+ homepage_uri: https://github.com/johannesdwicahyo/prompter-ruby
68
+ source_code_uri: https://github.com/johannesdwicahyo/prompter-ruby
69
+ changelog_uri: https://github.com/johannesdwicahyo/prompter-ruby/blob/main/CHANGELOG.md
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: 3.0.0
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubygems_version: 3.6.9
85
+ specification_version: 4
86
+ summary: Prompt template engine for Ruby
87
+ test_files: []