Module: Textus::Mustache
- Defined in:
- lib/textus/mustache.rb
Constant Summary collapse
- MAX_DEPTH =
8- TAG =
%r{\{\{(?<sigil>[#^/!&]?)\s*(?<name>[\w.-]+)\s*\}\}}
Class Method Summary collapse
- .falsy?(v) ⇒ Boolean
- .lookup(context, name) ⇒ Object
- .merge(base, override) ⇒ Object
- .parse_section(template, open_match, name) ⇒ Object
-
.render(template, context, strict: false, depth: 0) ⇒ Object
rubocop:disable Metrics/AbcSize.
- .render_section(section, value, context, strict, depth) ⇒ Object
-
.scope_for(context, item) ⇒ Object
Build the rendering scope for one iteration of a section.
Class Method Details
.falsy?(v) ⇒ Boolean
115 |
# File 'lib/textus/mustache.rb', line 115 def self.falsy?(v) = v.nil? || v == false || v == [] || v == "" |
.lookup(context, name) ⇒ Object
86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/textus/mustache.rb', line 86 def self.lookup(context, name) # Implicit iterator: {{.}} refers to the current scope itself (used when # iterating arrays of primitive values). return context["."] if name == "." && context.is_a?(Hash) && context.key?(".") return context[name] if context.is_a?(Hash) && context.key?(name) name.split(".").reduce(context) do |acc, seg| return nil unless acc.is_a?(Hash) acc[seg] end end |
.merge(base, override) ⇒ Object
109 110 111 112 113 |
# File 'lib/textus/mustache.rb', line 109 def self.merge(base, override) return base unless override.is_a?(Hash) base.merge(override) end |
.parse_section(template, open_match, name) ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/textus/mustache.rb', line 48 def self.parse_section(template, open_match, name) open_re = /\{\{#\s*#{Regexp.escape(name)}\s*\}\}|\{\{\^\s*#{Regexp.escape(name)}\s*\}\}/ close_re = %r{\{\{/\s*#{Regexp.escape(name)}\s*\}\}} both = Regexp.union(open_re, close_re) depth = 1 cursor = open_match.end(0) while (m = template.match(both, cursor)) if m[0].start_with?("{{/") depth -= 1 return [template[open_match.end(0)...m.begin(0)], m.end(0)] if depth.zero? else depth += 1 end cursor = m.end(0) end raise TemplateError.new("unclosed section: #{name}") end |
.render(template, context, strict: false, depth: 0) ⇒ Object
rubocop:disable Metrics/AbcSize
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/textus/mustache.rb', line 6 def self.render(template, context, strict: false, depth: 0) # rubocop:disable Metrics/AbcSize raise TemplateError.new("template recursion depth #{depth} exceeded #{MAX_DEPTH}") if depth > MAX_DEPTH out = +"" pos = 0 while (m = template.match(TAG, pos)) out << template[pos...m.begin(0)] case m[:sigil] when "!" # comment, skip when "#" section, new_pos = parse_section(template, m, m[:name]) value = lookup(context, m[:name]) out << render_section(section, value, context, strict, depth) pos = new_pos next when "^" section, new_pos = parse_section(template, m, m[:name]) value = lookup(context, m[:name]) if falsy?(value) raise TemplateError.new("template recursion depth #{depth + 1} exceeded #{MAX_DEPTH}") if depth + 1 > MAX_DEPTH out << render(section, context, strict: strict, depth: depth + 1) end pos = new_pos next when "/" raise TemplateError.new("unexpected closing tag #{m[:name]}") else val = lookup(context, m[:name]) if val.nil? raise TemplateError.new("missing variable: #{m[:name]}") if strict else out << val.to_s end end pos = m.end(0) end out << template[pos..] out end |
.render_section(section, value, context, strict, depth) ⇒ Object
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/textus/mustache.rb', line 66 def self.render_section(section, value, context, strict, depth) raise TemplateError.new("template recursion depth #{depth + 1} exceeded #{MAX_DEPTH}") if depth + 1 > MAX_DEPTH case value when Array value.map { |v| render(section, scope_for(context, v), strict: strict, depth: depth + 1) }.join when Hash render(section, merge(context, value), strict: strict, depth: depth + 1) when true render(section, context, strict: strict, depth: depth + 1) when false, nil # falsy in regular section: render nothing. # render_section is only called for inverted sections when falsy? is true at the call site, # so this branch is only hit for normal sections with falsy values. "" else render(section, context, strict: strict, depth: depth + 1) end || "" end |
.scope_for(context, item) ⇒ Object
Build the rendering scope for one iteration of a section. Hash items merge into the outer context; primitive items (strings, numbers) bind to the implicit iterator under key “.”.
102 103 104 105 106 107 |
# File 'lib/textus/mustache.rb', line 102 def self.scope_for(context, item) return merge(context, item) if item.is_a?(Hash) base = context.is_a?(Hash) ? context : {} base.merge("." => item) end |