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

Class Method Details

.falsy?(v) ⇒ Boolean

Returns:

  • (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

Raises:



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

Raises:



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

Raises:



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