braintrust 0.1.1 → 0.1.2
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/lib/braintrust/api/functions.rb +11 -0
- data/lib/braintrust/internal/template.rb +91 -0
- data/lib/braintrust/prompt.rb +143 -0
- data/lib/braintrust/vendor/mustache/context.rb +180 -0
- data/lib/braintrust/vendor/mustache/context_miss.rb +22 -0
- data/lib/braintrust/vendor/mustache/enumerable.rb +14 -0
- data/lib/braintrust/vendor/mustache/generator.rb +188 -0
- data/lib/braintrust/vendor/mustache/mustache.rb +260 -0
- data/lib/braintrust/vendor/mustache/parser.rb +364 -0
- data/lib/braintrust/vendor/mustache/settings.rb +252 -0
- data/lib/braintrust/vendor/mustache/template.rb +138 -0
- data/lib/braintrust/vendor/mustache/utils.rb +42 -0
- data/lib/braintrust/vendor/mustache.rb +16 -0
- data/lib/braintrust/version.rb +1 -1
- data/lib/braintrust.rb +1 -0
- metadata +13 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Vendored from mustache gem v1.1.1
|
|
4
|
+
# https://github.com/mustache/mustache
|
|
5
|
+
# License: MIT
|
|
6
|
+
# Modifications:
|
|
7
|
+
# - Namespaced under Braintrust::Vendor
|
|
8
|
+
# - Disabled HTML escaping (LLM prompts don't need HTML escaping)
|
|
9
|
+
|
|
10
|
+
require_relative "enumerable"
|
|
11
|
+
require_relative "template"
|
|
12
|
+
require_relative "context"
|
|
13
|
+
require_relative "settings"
|
|
14
|
+
require_relative "utils"
|
|
15
|
+
|
|
16
|
+
module Braintrust
|
|
17
|
+
module Vendor
|
|
18
|
+
# Mustache is the base class from which your Mustache subclasses
|
|
19
|
+
# should inherit (though it can be used on its own).
|
|
20
|
+
#
|
|
21
|
+
# The typical Mustache workflow is as follows:
|
|
22
|
+
#
|
|
23
|
+
# * Create a Mustache subclass: class Stats < Mustache
|
|
24
|
+
# * Create a template: stats.mustache
|
|
25
|
+
# * Instantiate an instance: view = Stats.new
|
|
26
|
+
# * Render that instance: view.render
|
|
27
|
+
#
|
|
28
|
+
# You can skip the instantiation by calling `Stats.render` directly.
|
|
29
|
+
class Mustache
|
|
30
|
+
# Initialize a new Mustache instance.
|
|
31
|
+
#
|
|
32
|
+
# @param [Hash] options An options hash
|
|
33
|
+
# @option options [String] template_path
|
|
34
|
+
# @option options [String] template_extension
|
|
35
|
+
# @option options [String] template_file
|
|
36
|
+
# @option options [String] template
|
|
37
|
+
# @option options [String] view_namespace
|
|
38
|
+
# @option options [String] view_path
|
|
39
|
+
def initialize(options = {})
|
|
40
|
+
@options = options
|
|
41
|
+
|
|
42
|
+
initialize_settings
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Instantiates an instance of this class and calls `render` with
|
|
46
|
+
# the passed args.
|
|
47
|
+
#
|
|
48
|
+
# @return A rendered String version of a template.
|
|
49
|
+
def self.render(*args)
|
|
50
|
+
new.render(*args)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Parses our fancy pants template file and returns normal file with
|
|
54
|
+
# all special {{tags}} and {{#sections}}replaced{{/sections}}.
|
|
55
|
+
#
|
|
56
|
+
# @example Render view
|
|
57
|
+
# @view.render("Hi {{thing}}!", :thing => :world)
|
|
58
|
+
#
|
|
59
|
+
# @example Set view template and then render
|
|
60
|
+
# View.template = "Hi {{thing}}!"
|
|
61
|
+
# @view = View.new
|
|
62
|
+
# @view.render(:thing => :world)
|
|
63
|
+
#
|
|
64
|
+
# @param [String,Hash] data A String template or a Hash context.
|
|
65
|
+
# If a Hash is given, we'll try to figure
|
|
66
|
+
# out the template from the class.
|
|
67
|
+
# @param [Hash] ctx A Hash context if `data` is a String template.
|
|
68
|
+
# @return [String] Returns a rendered version of a template.
|
|
69
|
+
def render(data = template, ctx = {})
|
|
70
|
+
case data
|
|
71
|
+
when Hash
|
|
72
|
+
ctx = data
|
|
73
|
+
when Symbol
|
|
74
|
+
self.template_name = data
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
tpl = case data
|
|
78
|
+
when Hash
|
|
79
|
+
templateify(template)
|
|
80
|
+
when Symbol
|
|
81
|
+
templateify(template)
|
|
82
|
+
else
|
|
83
|
+
templateify(data)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
return tpl.render(context) if ctx == {}
|
|
87
|
+
|
|
88
|
+
begin
|
|
89
|
+
context.push(ctx)
|
|
90
|
+
tpl.render(context)
|
|
91
|
+
ensure
|
|
92
|
+
context.pop
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Context accessors.
|
|
97
|
+
#
|
|
98
|
+
# @example Context accessors
|
|
99
|
+
# view = Mustache.new
|
|
100
|
+
# view[:name] = "Jon"
|
|
101
|
+
# view.template = "Hi, {{name}}!"
|
|
102
|
+
# view.render # => "Hi, Jon!"
|
|
103
|
+
def [](key)
|
|
104
|
+
context[key.to_sym]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def []=(key, value)
|
|
108
|
+
context[key.to_sym] = value
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A helper method which gives access to the context at a given time.
|
|
112
|
+
# Kind of a hack for now, but useful when you're in an iterating section
|
|
113
|
+
# and want access to the hash currently being iterated over.
|
|
114
|
+
def context
|
|
115
|
+
@context ||= Context.new(self)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Given a file name and an optional context, attempts to load and
|
|
119
|
+
# render the file as a template.
|
|
120
|
+
def self.render_file(name, context = {})
|
|
121
|
+
render(partial(name), context)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Given a file name and an optional context, attempts to load and
|
|
125
|
+
# render the file as a template.
|
|
126
|
+
def render_file(name, context = {})
|
|
127
|
+
self.class.render_file(name, context)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Given a name, attempts to read a file and return the contents as a
|
|
131
|
+
# string. The file is not rendered, so it might contain
|
|
132
|
+
# {{mustaches}}.
|
|
133
|
+
#
|
|
134
|
+
# Call `render` if you need to process it.
|
|
135
|
+
def self.partial(name)
|
|
136
|
+
new.partial(name)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Override this in your subclass if you want to do fun things like
|
|
140
|
+
# reading templates from a database. It will be rendered by the
|
|
141
|
+
# context, so all you need to do is return a string.
|
|
142
|
+
def partial(name)
|
|
143
|
+
path = "#{template_path}/#{name}.#{template_extension}"
|
|
144
|
+
|
|
145
|
+
begin
|
|
146
|
+
File.read(path)
|
|
147
|
+
rescue
|
|
148
|
+
raise if raise_on_context_miss?
|
|
149
|
+
""
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# BRAINTRUST MODIFICATION: No HTML escaping for LLM prompts.
|
|
154
|
+
# Original mustache uses CGI.escapeHTML which would turn
|
|
155
|
+
# characters like < > & " into HTML entities.
|
|
156
|
+
# For LLM prompts, we want the raw text without escaping.
|
|
157
|
+
#
|
|
158
|
+
# @param [Object] value Value to escape.
|
|
159
|
+
# @return [String] Unescaped content (just converted to string).
|
|
160
|
+
def escape(value)
|
|
161
|
+
value.to_s
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# @deprecated Use {#escape} instead.
|
|
165
|
+
# Kept for compatibility but also does no escaping.
|
|
166
|
+
def escapeHTML(str)
|
|
167
|
+
str.to_s
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Has this instance or its class already compiled a template?
|
|
171
|
+
def compiled?
|
|
172
|
+
(@template && @template.is_a?(Template)) || self.class.compiled?
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
# When given a symbol or string representing a class, will try to produce an
|
|
178
|
+
# appropriate view class.
|
|
179
|
+
#
|
|
180
|
+
# @example
|
|
181
|
+
# Mustache.view_namespace = Hurl::Views
|
|
182
|
+
# Mustache.view_class(:Partial) # => Hurl::Views::Partial
|
|
183
|
+
def self.view_class(name)
|
|
184
|
+
name = classify(name.to_s)
|
|
185
|
+
|
|
186
|
+
# Emptiness begets emptiness.
|
|
187
|
+
return Mustache if name.to_s.empty?
|
|
188
|
+
|
|
189
|
+
name = "#{view_namespace}::#{name}"
|
|
190
|
+
const = rescued_const_get(name)
|
|
191
|
+
|
|
192
|
+
return const if const
|
|
193
|
+
|
|
194
|
+
const_from_file(name)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def self.rescued_const_get(name)
|
|
198
|
+
const_get(name, true) || Mustache
|
|
199
|
+
rescue NameError
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def self.const_from_file(name)
|
|
204
|
+
file_name = underscore(name)
|
|
205
|
+
file_path = "#{view_path}/#{file_name}.rb"
|
|
206
|
+
|
|
207
|
+
return Mustache unless File.exist?(file_path)
|
|
208
|
+
|
|
209
|
+
require file_path.chomp(".rb")
|
|
210
|
+
rescued_const_get(name)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Has this template already been compiled? Compilation is somewhat
|
|
214
|
+
# expensive so it may be useful to check this before attempting it.
|
|
215
|
+
def self.compiled?
|
|
216
|
+
@template.is_a? Template
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# template_partial => TemplatePartial
|
|
220
|
+
# template/partial => Template::Partial
|
|
221
|
+
def self.classify(underscored)
|
|
222
|
+
Mustache::Utils::String.new(underscored).classify
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# TemplatePartial => template_partial
|
|
226
|
+
# Template::Partial => template/partial
|
|
227
|
+
# Takes a string but defaults to using the current class' name.
|
|
228
|
+
def self.underscore(classified = name)
|
|
229
|
+
classified = superclass.name if classified.to_s.empty?
|
|
230
|
+
|
|
231
|
+
Mustache::Utils::String.new(classified).underscore(view_namespace)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# @param [Template,String] obj Turns `obj` into a template
|
|
235
|
+
# @param [Hash] options Options for template creation
|
|
236
|
+
def self.templateify(obj, options = {})
|
|
237
|
+
obj.is_a?(Template) ? obj : Template.new(obj, options)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def templateify(obj)
|
|
241
|
+
opts = {partial_resolver: method(:partial)}
|
|
242
|
+
opts.merge!(@options) if @options.is_a?(Hash)
|
|
243
|
+
self.class.templateify(obj, opts)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Return the value of the configuration setting on the superclass, or return
|
|
247
|
+
# the default.
|
|
248
|
+
#
|
|
249
|
+
# @param [Symbol] attr_name Name of the attribute. It should match
|
|
250
|
+
# the instance variable.
|
|
251
|
+
# @param [Object] default Default value to use if the superclass does
|
|
252
|
+
# not respond.
|
|
253
|
+
#
|
|
254
|
+
# @return Inherited or default configuration setting.
|
|
255
|
+
def self.inheritable_config_for(attr_name, default)
|
|
256
|
+
superclass.respond_to?(attr_name) ? superclass.send(attr_name) : default
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Vendored from mustache gem v1.1.1
|
|
4
|
+
# https://github.com/mustache/mustache
|
|
5
|
+
# License: MIT
|
|
6
|
+
# Modifications: Namespaced under Braintrust::Vendor
|
|
7
|
+
|
|
8
|
+
require "strscan"
|
|
9
|
+
|
|
10
|
+
module Braintrust
|
|
11
|
+
module Vendor
|
|
12
|
+
class Mustache
|
|
13
|
+
# The Parser is responsible for taking a string template and
|
|
14
|
+
# converting it into an array of tokens and, really, expressions. It
|
|
15
|
+
# raises SyntaxError if there is anything it doesn't understand and
|
|
16
|
+
# knows which sigil corresponds to which tag type.
|
|
17
|
+
#
|
|
18
|
+
# For example, given this template:
|
|
19
|
+
#
|
|
20
|
+
# Hi {{thing}}!
|
|
21
|
+
#
|
|
22
|
+
# Run through the Parser we'll get these tokens:
|
|
23
|
+
#
|
|
24
|
+
# [:multi,
|
|
25
|
+
# [:static, "Hi "],
|
|
26
|
+
# [:mustache, :etag, "thing"],
|
|
27
|
+
# [:static, "!\n"]]
|
|
28
|
+
class Parser
|
|
29
|
+
# A SyntaxError is raised when the Parser comes across unclosed
|
|
30
|
+
# tags, sections, illegal content in tags, or anything of that
|
|
31
|
+
# sort.
|
|
32
|
+
class SyntaxError < StandardError
|
|
33
|
+
def initialize(message, position)
|
|
34
|
+
@message = message
|
|
35
|
+
@lineno, @column, @line, _ = position
|
|
36
|
+
@stripped_line = @line.strip
|
|
37
|
+
@stripped_column = @column - (@line.size - @line.lstrip.size)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_s
|
|
41
|
+
<<-EOF
|
|
42
|
+
#{@message}
|
|
43
|
+
Line #{@lineno}
|
|
44
|
+
#{@stripped_line}
|
|
45
|
+
#{" " * @stripped_column}^
|
|
46
|
+
EOF
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The sigil types which are valid after an opening `{{`
|
|
51
|
+
VALID_TYPES = ["#", "^", "/", "=", "!", "<", ">", "&", "{"].map(&:freeze)
|
|
52
|
+
|
|
53
|
+
def self.valid_types
|
|
54
|
+
@valid_types ||= Regexp.new(VALID_TYPES.map { |t| Regexp.escape(t) }.join("|"))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Add a supported sigil type (with optional aliases) to the Parser.
|
|
58
|
+
#
|
|
59
|
+
# Requires a block, which will be sent the following parameters:
|
|
60
|
+
#
|
|
61
|
+
# * content - The raw content of the tag
|
|
62
|
+
# * fetch- A mustache context fetch expression for the content
|
|
63
|
+
# * padding - Indentation whitespace from the currently-parsed line
|
|
64
|
+
# * pre_match_position - Location of the scanner before a match was made
|
|
65
|
+
#
|
|
66
|
+
# The provided block will be evaluated against the current instance of
|
|
67
|
+
# Parser, and may append to the Parser's @result as needed.
|
|
68
|
+
def self.add_type(*types, &block)
|
|
69
|
+
types = types.map(&:to_s)
|
|
70
|
+
type, *aliases = types
|
|
71
|
+
method_name = :"scan_tag_#{type}"
|
|
72
|
+
define_method(method_name, &block)
|
|
73
|
+
aliases.each { |a| alias_method :"scan_tag_#{a}", method_name }
|
|
74
|
+
types.each { |t| VALID_TYPES << t unless VALID_TYPES.include?(t) }
|
|
75
|
+
@valid_types = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# After these types of tags, all whitespace until the end of the line will
|
|
79
|
+
# be skipped if they are the first (and only) non-whitespace content on
|
|
80
|
+
# the line.
|
|
81
|
+
SKIP_WHITESPACE = ["#", "^", "/", "<", ">", "=", "!"].map(&:freeze)
|
|
82
|
+
|
|
83
|
+
# The content allowed in a tag name.
|
|
84
|
+
ALLOWED_CONTENT = /(\w|[?!\/.=-])*/
|
|
85
|
+
|
|
86
|
+
# These types of tags allow any content,
|
|
87
|
+
# the rest only allow ALLOWED_CONTENT.
|
|
88
|
+
ANY_CONTENT = ["!", "="].map(&:freeze)
|
|
89
|
+
|
|
90
|
+
attr_reader :otag, :ctag
|
|
91
|
+
|
|
92
|
+
# Accepts an options hash which does nothing but may be used in
|
|
93
|
+
# the future.
|
|
94
|
+
def initialize(options = {})
|
|
95
|
+
@options = options
|
|
96
|
+
@option_inline_partials_at_compile_time = options[:inline_partials_at_compile_time]
|
|
97
|
+
if @option_inline_partials_at_compile_time
|
|
98
|
+
@partial_resolver = options[:partial_resolver]
|
|
99
|
+
raise ArgumentError.new "Missing or invalid partial_resolver" unless @partial_resolver.respond_to? :call
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Initialize default tags
|
|
103
|
+
self.otag ||= "{{"
|
|
104
|
+
self.ctag ||= "}}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The opening tag delimiter. This may be changed at runtime.
|
|
108
|
+
def otag=(value)
|
|
109
|
+
regex = regexp value
|
|
110
|
+
@otag_regex = /([ \t]*)?#{regex}/
|
|
111
|
+
@otag_not_regex = /(^[ \t]*)?#{regex}/
|
|
112
|
+
@otag = value
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The closing tag delimiter. This too may be changed at runtime.
|
|
116
|
+
def ctag=(value)
|
|
117
|
+
@ctag_regex = regexp value
|
|
118
|
+
@ctag = value
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Given a string template, returns an array of tokens.
|
|
122
|
+
def compile(template)
|
|
123
|
+
@encoding = nil
|
|
124
|
+
|
|
125
|
+
if template.respond_to?(:encoding)
|
|
126
|
+
@encoding = template.encoding
|
|
127
|
+
template = template.dup.force_encoding("BINARY")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Keeps information about opened sections.
|
|
131
|
+
@sections = []
|
|
132
|
+
@result = [:multi]
|
|
133
|
+
@scanner = StringScanner.new(template)
|
|
134
|
+
|
|
135
|
+
# Scan until the end of the template.
|
|
136
|
+
until @scanner.eos?
|
|
137
|
+
scan_tags || scan_text
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
unless @sections.empty?
|
|
141
|
+
# We have parsed the whole file, but there's still opened sections.
|
|
142
|
+
type, pos, _ = @sections.pop
|
|
143
|
+
error "Unclosed section #{type.inspect}", pos
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
@result
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def content_tags(type, current_ctag_regex)
|
|
152
|
+
if ANY_CONTENT.include?(type)
|
|
153
|
+
r = /\s*#{regexp(type)}?#{current_ctag_regex}/
|
|
154
|
+
scan_until_exclusive(r)
|
|
155
|
+
else
|
|
156
|
+
@scanner.scan(ALLOWED_CONTENT)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def dispatch_based_on_type(type, content, fetch, padding, pre_match_position)
|
|
161
|
+
send(:"scan_tag_#{type}", content, fetch, padding, pre_match_position)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def find_closing_tag(scanner, current_ctag_regex)
|
|
165
|
+
error "Unclosed tag" unless scanner.scan(current_ctag_regex)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Find {{mustaches}} and add them to the @result array.
|
|
169
|
+
def scan_tags
|
|
170
|
+
# Scan until we hit an opening delimiter.
|
|
171
|
+
start_of_line = @scanner.beginning_of_line?
|
|
172
|
+
pre_match_position = @scanner.pos
|
|
173
|
+
last_index = @result.length
|
|
174
|
+
|
|
175
|
+
return unless @scanner.scan @otag_regex
|
|
176
|
+
padding = @scanner[1] || ""
|
|
177
|
+
|
|
178
|
+
# Don't touch the preceding whitespace unless we're matching the start
|
|
179
|
+
# of a new line.
|
|
180
|
+
unless start_of_line
|
|
181
|
+
@result << [:static, padding] unless padding.empty?
|
|
182
|
+
pre_match_position += padding.length
|
|
183
|
+
padding = ""
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Since {{= rewrites ctag, we store the ctag which should be used
|
|
187
|
+
# when parsing this specific tag.
|
|
188
|
+
current_ctag_regex = @ctag_regex
|
|
189
|
+
type = @scanner.scan(self.class.valid_types)
|
|
190
|
+
@scanner.skip(/\s*/)
|
|
191
|
+
|
|
192
|
+
# ANY_CONTENT tags allow any character inside of them, while
|
|
193
|
+
# other tags (such as variables) are more strict.
|
|
194
|
+
content = content_tags(type, current_ctag_regex)
|
|
195
|
+
|
|
196
|
+
# We found {{ but we can't figure out what's going on inside.
|
|
197
|
+
error "Illegal content in tag" if content.empty?
|
|
198
|
+
|
|
199
|
+
fetch = [:mustache, :fetch, content.split(".")]
|
|
200
|
+
prev = @result
|
|
201
|
+
|
|
202
|
+
dispatch_based_on_type(type, content, fetch, padding, pre_match_position)
|
|
203
|
+
|
|
204
|
+
# The closing } in unescaped tags is just a hack for
|
|
205
|
+
# aesthetics.
|
|
206
|
+
type = "}" if type == "{"
|
|
207
|
+
|
|
208
|
+
# Skip whitespace and any balancing sigils after the content
|
|
209
|
+
# inside this tag.
|
|
210
|
+
@scanner.skip(/\s+/)
|
|
211
|
+
@scanner.skip(regexp(type)) if type
|
|
212
|
+
|
|
213
|
+
find_closing_tag(@scanner, current_ctag_regex)
|
|
214
|
+
|
|
215
|
+
# If this tag was the only non-whitespace content on this line, strip
|
|
216
|
+
# the remaining whitespace. If not, but we've been hanging on to padding
|
|
217
|
+
# from the beginning of the line, re-insert the padding as static text.
|
|
218
|
+
if start_of_line && !@scanner.eos?
|
|
219
|
+
if @scanner.peek(2) =~ /\r?\n/ && SKIP_WHITESPACE.include?(type)
|
|
220
|
+
@scanner.skip(/\r?\n/)
|
|
221
|
+
else
|
|
222
|
+
prev.insert(last_index, [:static, padding]) unless padding.empty?
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Store off the current scanner position now that we've closed the tag
|
|
227
|
+
# and consumed any irrelevant whitespace.
|
|
228
|
+
@sections.last[1] << @scanner.pos unless @sections.empty?
|
|
229
|
+
|
|
230
|
+
return unless @result == [:multi]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Try to find static text, e.g. raw HTML with no {{mustaches}}.
|
|
234
|
+
def scan_text
|
|
235
|
+
text = scan_until_exclusive @otag_not_regex
|
|
236
|
+
|
|
237
|
+
if text.nil?
|
|
238
|
+
# Couldn't find any otag, which means the rest is just static text.
|
|
239
|
+
text = @scanner.rest
|
|
240
|
+
# Mark as done.
|
|
241
|
+
@scanner.terminate
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
text.force_encoding(@encoding) if @encoding
|
|
245
|
+
|
|
246
|
+
@result << [:static, text] unless text.empty?
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Scans the string until the pattern is matched. Returns the substring
|
|
250
|
+
# *excluding* the end of the match, advancing the scan pointer to that
|
|
251
|
+
# location. If there is no match, nil is returned.
|
|
252
|
+
def scan_until_exclusive(regexp)
|
|
253
|
+
pos = @scanner.pos
|
|
254
|
+
if @scanner.scan_until(regexp)
|
|
255
|
+
@scanner.pos -= @scanner.matched.size
|
|
256
|
+
@scanner.pre_match[pos..-1]
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def offset
|
|
261
|
+
position[0, 2]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Returns [lineno, column, line]
|
|
265
|
+
def position
|
|
266
|
+
# The rest of the current line
|
|
267
|
+
rest = @scanner.check_until(/\n|\Z/).to_s.chomp
|
|
268
|
+
|
|
269
|
+
# What we have parsed so far
|
|
270
|
+
parsed = @scanner.string[0...@scanner.pos]
|
|
271
|
+
|
|
272
|
+
lines = parsed.split("\n")
|
|
273
|
+
|
|
274
|
+
[lines.size, lines.last.size - 1, lines.last + rest]
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Used to quickly convert a string into a regular expression
|
|
278
|
+
# usable by the string scanner.
|
|
279
|
+
def regexp(thing)
|
|
280
|
+
Regexp.new Regexp.escape(thing) if thing
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Raises a SyntaxError. The message should be the name of the
|
|
284
|
+
# error - other details such as line number and position are
|
|
285
|
+
# handled for you.
|
|
286
|
+
def error(message, pos = position)
|
|
287
|
+
raise SyntaxError.new(message, pos)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
#
|
|
291
|
+
# Scan tags
|
|
292
|
+
#
|
|
293
|
+
# These methods are called in `scan_tags`. Because they contain nonstandard
|
|
294
|
+
# characters in their method names, they are aliased to
|
|
295
|
+
# better named methods.
|
|
296
|
+
#
|
|
297
|
+
|
|
298
|
+
# This function handles the cases where the scanned tag does not have
|
|
299
|
+
# a type.
|
|
300
|
+
def scan_tag_(content, fetch, padding, pre_match_position)
|
|
301
|
+
@result << [:mustache, :etag, fetch, offset]
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def scan_tag_block(content, fetch, padding, pre_match_position)
|
|
305
|
+
block = [:multi]
|
|
306
|
+
@result << [:mustache, :section, fetch, offset, block]
|
|
307
|
+
@sections << [content, position, @result]
|
|
308
|
+
@result = block
|
|
309
|
+
end
|
|
310
|
+
alias_method :'scan_tag_#', :scan_tag_block
|
|
311
|
+
|
|
312
|
+
def scan_tag_inverted(content, fetch, padding, pre_match_position)
|
|
313
|
+
block = [:multi]
|
|
314
|
+
@result << [:mustache, :inverted_section, fetch, offset, block]
|
|
315
|
+
@sections << [content, position, @result]
|
|
316
|
+
@result = block
|
|
317
|
+
end
|
|
318
|
+
alias_method :'scan_tag_^', :scan_tag_inverted
|
|
319
|
+
|
|
320
|
+
def scan_tag_close(content, fetch, padding, pre_match_position)
|
|
321
|
+
section, pos, result = @sections.pop
|
|
322
|
+
if section.nil?
|
|
323
|
+
error "Closing unopened #{content.inspect}"
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
raw = @scanner.pre_match[pos[3]...pre_match_position] + padding
|
|
327
|
+
(@result = result).last << raw << [otag, ctag]
|
|
328
|
+
|
|
329
|
+
if section != content
|
|
330
|
+
error "Unclosed section #{section.inspect}", pos
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
alias_method :'scan_tag_/', :scan_tag_close
|
|
334
|
+
|
|
335
|
+
def scan_tag_comment(content, fetch, padding, pre_match_position)
|
|
336
|
+
end
|
|
337
|
+
alias_method :'scan_tag_!', :scan_tag_comment
|
|
338
|
+
|
|
339
|
+
def scan_tag_delimiter(content, fetch, padding, pre_match_position)
|
|
340
|
+
self.otag, self.ctag = content.split(" ", 2)
|
|
341
|
+
end
|
|
342
|
+
alias_method :'scan_tag_=', :scan_tag_delimiter
|
|
343
|
+
|
|
344
|
+
def scan_tag_open_partial(content, fetch, padding, pre_match_position)
|
|
345
|
+
@result << if @option_inline_partials_at_compile_time
|
|
346
|
+
partial = @partial_resolver.call content
|
|
347
|
+
partial.gsub!(/^/, padding) unless padding.empty?
|
|
348
|
+
self.class.new(@options).compile partial
|
|
349
|
+
else
|
|
350
|
+
[:mustache, :partial, content, offset, padding]
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
alias_method :'scan_tag_<', :scan_tag_open_partial
|
|
354
|
+
alias_method :'scan_tag_>', :scan_tag_open_partial
|
|
355
|
+
|
|
356
|
+
def scan_tag_unescaped(content, fetch, padding, pre_match_position)
|
|
357
|
+
@result << [:mustache, :utag, fetch, offset]
|
|
358
|
+
end
|
|
359
|
+
alias_method :'scan_tag_{', :scan_tag_unescaped
|
|
360
|
+
alias_method :'scan_tag_&', :scan_tag_unescaped
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|