Class: Aspen::Compiler

Inherits:
Object
  • Object
show all
Defined in:
lib/aspen/compiler.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root, environment = {}) ⇒ Compiler

TODO:

Make #environment an Aspen::Environment

Returns a new instance of Compiler.

Parameters:

  • environment (Hash) (defaults to: {})


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/aspen/compiler.rb', line 17

def initialize(root, environment = {})
  @root = root
  @environment = environment
  @adapter = environment.fetch(:adapter, :cypher).to_sym
  # @todo FIXME: This is too much responsibility for the compiler.
  #   This should be delegated to an object and the later calls
  #   just messages to that object.
  @slug_counters = Hash.new { 1 }

  # @todo Move this into an Environment object—it should be set there.
  #   and here, just run environment.validate
  unless Aspen.available_formats.include?(@adapter)
    raise Aspen::ArgumentError, <<~MSG
      The adapter, also known as the output format, must be one of:
      #{Aspen.available_formats.join(', ')}.

      What Aspen received was #{@adapter}.
    MSG
  end
end

Instance Attribute Details

#environmentObject (readonly)

Returns the value of attribute environment.



7
8
9
# File 'lib/aspen/compiler.rb', line 7

def environment
  @environment
end

#rootObject (readonly)

Returns the value of attribute root.



7
8
9
# File 'lib/aspen/compiler.rb', line 7

def root
  @root
end

Class Method Details

.render(root, environment = {}) ⇒ Object

TODO:

Make #environment an Aspen::Environment

Parameters:

  • environment (Hash) (defaults to: {})


11
12
13
# File 'lib/aspen/compiler.rb', line 11

def self.render(root, environment = {})
  new(root, environment).render
end

Instance Method Details

#discourseObject



42
43
44
# File 'lib/aspen/compiler.rb', line 42

def discourse
  @discourse ||= Discourse.from_hash(environment)
end

#renderObject



38
39
40
# File 'lib/aspen/compiler.rb', line 38

def render
  visit(root)
end

#visit(node) ⇒ Object



46
47
48
49
50
# File 'lib/aspen/compiler.rb', line 46

def visit(node)
  short_name = node.class.to_s.split('::').last.downcase
  method_name = "visit_#{short_name}"
  send(method_name, node)
end

#visit_attribute(node) ⇒ Object



189
190
191
192
193
# File 'lib/aspen/compiler.rb', line 189

def visit_attribute(node)
  content = visit(node.content)
  type    = visit(node.type)
  content.send(type.converter)
end

#visit_comment(node) ⇒ Object

This acts as a signal so other methods know to reject comments.



205
206
207
# File 'lib/aspen/compiler.rb', line 205

def visit_comment(node)
  :comment
end

#visit_content(node) ⇒ Object



199
200
201
# File 'lib/aspen/compiler.rb', line 199

def visit_content(node)
  node.content
end

#visit_customstatement(node) ⇒ Object

TODO:

Get the labels back into here. Labelreg? typereg? This is doing too much. Can’t we have typed attributes come from the Grammar?



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/aspen/compiler.rb', line 76

def visit_customstatement(node)
  statement = visit(node.content)
  matcher   = discourse.grammar.matcher_for(statement)
  results   = matcher.captures(statement)
  template  = matcher.template
  typereg   = matcher.typereg
  labelreg  = matcher.labelreg

  nodes = []

  typed_results = results.inject({}) do |hash, elem|
    key, value = elem
    typed_value = case typereg[key]
    when :integer then value.to_i
    when :float   then value.to_f
    when :numeric then
      value.match?(/\./) ? value.to_f : value.to_i
    when :string  then "\"#{value}\""
    when :node    then
      # FIXME: This only handles short form.
      #   I think we were allowing grouped and Cypher form to fill
      #   in custom statement templates.
      # TODO: Add some object to nodes array.
      node = visit(
        Aspen::AST::Nodes::Node.new(
          attribute: value,
          label: labelreg[key]
        )
      )
      nodes << node
      node
    end
    hash[key] = typed_value
    hash
  end

  formatted_results = typed_results.inject({}) do |hash, elem|
    key, value = elem
    f_value = value.is_a?(Aspen::Node) ? value.nickname_node : value
    hash[key] = f_value

    # TODO: Trying to insert a p_id as well as p to be used in JSON identifiers.
    # if value.is_a?(Aspen::Node)
    #   hash["#{key}_id"] = value.nickname
    # end
    # puts "TYPED VALS: #{hash.inspect}"
    hash
  end

  slugs = template.scan(/{{{?(?<full>uniq_(?<name>\w+))}}}?/).uniq
  usable_results = if slugs.any?
    counts = slugs.map do |full, short|
      [full, "#{short}_#{@slug_counters[full]}"]
    end.to_h

    context = results.merge(counts)
    custom_statement = CustomStatement.new(
      nodes: nodes,
      cypher: Mustache.render(template.strip, formatted_results.merge(counts))
    )
    slugs.each do |full, _|
      @slug_counters[full] = @slug_counters[full] + 1
    end
    custom_statement
  else
    CustomStatement.new(
      nodes: nodes,
      cypher: Mustache.render(template.strip, formatted_results)
    )
  end
end

#visit_edge(node) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/aspen/compiler.rb', line 163

def visit_edge(node)
  content = visit(node.content)
  unless discourse.allows_edge?(content)
    raise Aspen::Error, """
      Your narrative includes an edge called '#{content}',
      but only #{discourse.allowed_edges} are allowed.
    """
  end
  Aspen::Edge.new(
    content,
    mutual: discourse.mutual?(visit(node.content))
  )
end

#visit_label(node) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
# File 'lib/aspen/compiler.rb', line 177

def visit_label(node)
  content = visit(node.content)
  label = Maybe(content).value_or(discourse.default_label)
  unless discourse.allows_label?(label)
    raise Aspen::CompileError, """
      Your narrative includes a node with label '#{label}',
      but only #{discourse.allowed_labels} are allowed.
    """
  end
  label
end

#visit_narrative(node) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/aspen/compiler.rb', line 52

def visit_narrative(node)
  # Instead of letting comments be `nil` and using `#compact`
  # to silently remove them, possibly hiding errors, we "compile"
  # comments as `:comment` and filter them explicitly
  statements = node.statements.map do |statement|
    # This will visit both regular and custom statements.
    visit(statement)
  end.reject { |elem| elem == :comment }

  renderer_klass = Kernel.const_get("Aspen::Renderers::#{@adapter.to_s.downcase.capitalize}Renderer")
  renderer_klass.new(statements, environment).render
end

#visit_node(node) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/aspen/compiler.rb', line 148

def visit_node(node)
  # Get the label, falling back to the default label.
  label = visit(node.label)

  # Get the attribute name, falling back to the default attribute name.
  attribute_name  = Maybe(nil).value_or(discourse.default_attr_name(label))
  typed_attribute_value = visit(node.attribute)
  nickname = typed_attribute_value.to_s.downcase

  Aspen::Node.new(
    label: label,
    attributes: { attribute_name => typed_attribute_value }
  )
end

#visit_statement(node) ⇒ Object



65
66
67
68
69
70
71
# File 'lib/aspen/compiler.rb', line 65

def visit_statement(node)
  Statement.new(
    origin: visit(node.origin),
    edge: visit(node.edge),
    target: visit(node.target)
  )
end

#visit_type(node) ⇒ Object



195
196
197
# File 'lib/aspen/compiler.rb', line 195

def visit_type(node)
  node
end