Class: Opulent::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/opulent/parser.rb,
lib/opulent/parser/eval.rb,
lib/opulent/parser/node.rb,
lib/opulent/parser/root.rb,
lib/opulent/parser/text.rb,
lib/opulent/parser/yield.rb,
lib/opulent/parser/define.rb,
lib/opulent/parser/filter.rb,
lib/opulent/parser/comment.rb,
lib/opulent/parser/control.rb,
lib/opulent/parser/doctype.rb,
lib/opulent/parser/include.rb,
lib/opulent/parser/expression.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(settings = {}) ⇒ Parser

All node Objects (Array) must follow the next convention in order to make parsing faster

:node_type, :value, :attributes, :children, :indent


25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/opulent/parser.rb', line 25

def initialize(settings = {})
  # Convention accessors
  @type = 0
  @value = 1
  @options = 2
  @children = 3
  @indent = 4

  # Inherit settings from Engine
  @settings = settings

  # Set current compiled file as the first in the file stack together with
  # its base indentation. The stack is used to allow include directives to
  # be used with the last parent path found
  @file = [[@settings.delete(:file), -1]]

  # Create a definition stack to disallow recursive calls. When inside a
  # definition and a named node is called, we render it as a plain node
  @definition_stack = []

  # Initialize definitions for the parser
  @definitions = @settings.delete(:def) || {}
end

Instance Attribute Details

#childrenObject (readonly)

Returns the value of attribute children.



18
19
20
# File 'lib/opulent/parser.rb', line 18

def children
  @children
end

#definitionsObject (readonly)

Returns the value of attribute definitions.



18
19
20
# File 'lib/opulent/parser.rb', line 18

def definitions
  @definitions
end

#indentObject (readonly)

Returns the value of attribute indent.



18
19
20
# File 'lib/opulent/parser.rb', line 18

def indent
  @indent
end

#optionsObject (readonly)

Returns the value of attribute options.



18
19
20
# File 'lib/opulent/parser.rb', line 18

def options
  @options
end

#typeObject (readonly)

Returns the value of attribute type.



18
19
20
# File 'lib/opulent/parser.rb', line 18

def type
  @type
end

#valueObject (readonly)

Returns the value of attribute value.



18
19
20
# File 'lib/opulent/parser.rb', line 18

def value
  @value
end

Instance Method Details

#accept(token, required = false, strip = false) ⇒ Object

Check and accept or reject a given token as long as we have tokens remaining. Shift the code with the match length plus any extra character count around the capture group

Parameters:

  • token (RegEx)

    Token to be accepted by the parser

  • required (Boolean) (defaults to: false)

    Expect the given token syntax

  • strip (Boolean) (defaults to: false)

    Left strip the current code to remove whitespace



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/opulent/parser.rb', line 131

def accept(token, required = false, strip = false)
  # Consume leading whitespace if we want to ignore it
  accept :whitespace if strip

  # We reached the end of the parsing process and there are no more lines
  # left to parse
  return nil unless @line

  # Match the token to the current line. If we find it, return the match.
  # If it is required, signal an :expected error
  if (match = @line[@offset..-1].match(Tokens[token]))
    # Advance current offset with match length
    @offset += match[0].size
    @j += match[0].size

    return match[0]
  elsif required
    Logger.error :parse, @code, @i, @j, :expected, token
  end
end

#accept_newlineObject

Allow expressions to continue on a new line in certain conditions



198
199
200
201
202
203
# File 'lib/opulent/parser.rb', line 198

def accept_newline
  return unless @line[@offset..-1].strip.empty?
  @line = @code[(@i += 1)]
  @j = 0
  @offset = 0
end

#accept_stripped(token, required = false) ⇒ Object

Helper method which automatically sets the stripped options to true, so that we do not have to explicitly specify it

Parameters:

  • token (RegEx)

    Token to be accepted by the parser

  • required (Boolean) (defaults to: false)

    Expect the given token syntax



158
159
160
# File 'lib/opulent/parser.rb', line 158

def accept_stripped(token, required = false)
  accept(token, required, true)
end

#add_attribute(atts, key, value) ⇒ Object

Helper method to create an array of values when an attribute is set multiple times. This happens unless the key is id, which is unique

Parameters:

  • atts (Hash)

    Current node attributes hash

  • key (Symbol)

    Attribute name

  • value (String)

    Attribute value



144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/opulent/parser/node.rb', line 144

def add_attribute(atts, key, value)
  # Check for unique key and arrays of attributes
  if key == :class
    # If the key is already associated to an array, add the value to the
    # array, otherwise, create a new array or set it
    if atts[key]
      atts[key] << value
    else
      atts[key] = [value]
    end
  else
    atts[key] = value
  end
end

#apply_definitions(node) ⇒ Object

Set each node as a defined node if a definition exists with the given node name, unless we’re inside a definition with the same name as the node because we want to avoid recursion.

Parameters:

  • node (Node)

    Input Node to checked for existence of definitions



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
# File 'lib/opulent/parser.rb', line 93

def apply_definitions(node)
  # Apply definition check to all of the node's children
  process_definitions = proc do |children|
    children.each do |child|
      # Check if we have a definition
      is_definition = if child[@value] == @current_def
                        child[@options][:recursive]
                      else
                        @definitions.key?(child[@value])
                      end

      # Set child as a defined node
      child[@type] = :def if is_definition

      # Recursively apply definitions to child nodes
      apply_definitions child if child[@children]
    end
  end

  # Apply definitions to each case of the control node
  if [:if, :unless, :case].include? node[@type]
    node[@children].each do |array|
      process_definitions[array]
    end
  # Apply definition to all of the node's children
  else
    process_definitions[node[@children]]
  end
end

#arrayObject

Check if it’s possible to parse a ruby array literal. First, try to see if the next sequence is a hash_open token: “[”, and if it is, then a hash_close: “]” token is required next

array_elements
access][access


60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/opulent/parser/expression.rb', line 60

def array
  buffer = ''

  while (bracket = accept :square_bracket)
    buffer += bracket
    accept_newline
    buffer += array_elements
    accept_newline
    buffer += accept :'[', :*
  end

  buffer == '' ? nil : buffer
end

#array_elements(buffer = '') ⇒ Object

Recursively gather expressions separated by a comma and add them to the expression buffer

experssion1, experssion2, experssion3

Parameters:

  • buffer (String) (defaults to: '')

    Accumulator for the array elements



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/opulent/parser/expression.rb', line 81

def array_elements(buffer = '')
  if (term = expression)
    buffer += term[@value]
    # If there is an array_terminator ",", recursively gather the next
    # array element into the buffer
    if (terminator = accept_stripped :comma)
      accept_newline
      buffer += array_elements terminator
    end
  end

  # Array ended prematurely with a trailing comma, therefore the current
  # parsing process will stop
  error :array_elements_terminator if buffer.strip[-1] == ','

  buffer
end

#attributes(atts = {}, for_definition = false) ⇒ Object

Get element attributes

Parameters:

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

    Accumulator for node attributes

  • for_definition (Boolean) (defaults to: false)

    Set default value in wrapped to nil



195
196
197
198
199
# File 'lib/opulent/parser/node.rb', line 195

def attributes(atts = {}, for_definition = false)
  wrapped_attributes atts, for_definition
  attributes_assignments atts, false, for_definition
  atts
end

#attributes_assignments(list, wrapped = true, for_definition = false) ⇒ Object

Get all attribute assignments as key=value pairs or standalone keys

assignments

Parameters:

  • list (Hash)

    Parent to which we append nodes

  • as_parameters (Boolean)

    Accept or reject identifier nodes



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/opulent/parser/node.rb', line 226

def attributes_assignments(list, wrapped = true, for_definition = false)
  if wrapped && lookahead(:exp_identifier_stripped_lookahead).nil? ||
     !wrapped && lookahead(:assignment_lookahead).nil?
    return list
  end

  return unless (argument = accept_stripped :node)

  argument = argument.to_sym

  if accept_stripped :assignment
    # Check if we have an attribute escape or not
    escaped = if accept :assignment_unescaped
                false
              else
                true
              end

    # Get the argument value if we have an assignment
    if (value = expression(false, wrapped))
      value[@options][:escaped] = escaped

      # IDs are unique, the rest of the attributes turn into arrays in
      # order to allow multiple values or identifiers
      add_attribute(list, argument, value)
    else
      Logger.error :parse, @code, @i, @j, :assignments_colon
    end
  else
    unless list[argument]
      default_value = for_definition ? 'nil' : 'true'
      list[argument] = [:expression, default_value, { escaped: false }]
    end
  end

  # If our attributes are wrapped, we allow method calls without
  # paranthesis, ruby style, therefore we need a terminator to signify
  # the expression end. If they are not wrapped (inline), we require
  # paranthesis and allow inline calls
  if wrapped
    # Accept optional comma between attributes
    accept_stripped :assignment_terminator

    # Lookahead for attributes on the current line and the next one
    if lookahead(:exp_identifier_stripped_lookahead)
      attributes_assignments list, wrapped, for_definition
    elsif lookahead_next_line(:exp_identifier_stripped_lookahead)
      accept_newline
      attributes_assignments list, wrapped, for_definition
    end
  elsif !wrapped && lookahead(:assignment_lookahead)
    attributes_assignments list, wrapped, for_definition
  end

  list
end

#block_yield(parent, indent) ⇒ Object

Match a yield with a explicit or implicit target

yield target

Parameters:

  • parent (Node)

    Parent node to which we append the definition



11
12
13
14
15
16
17
18
19
20
21
# File 'lib/opulent/parser/yield.rb', line 11

def block_yield(parent, indent)
  return unless accept :yield

  # Consume the newline from the end of the element
  error :yield unless accept(:line_feed).strip.empty?

  # Create a new node
  yield_node = [:yield, nil, {}, [], indent]

  parent[@children] << yield_node
end

#callObject

Check if it’s possible to parse a ruby call literal. First, try to see if the next sequence is a hash_open token: “(”, and if it is, then a hash_close: “)” token is required next

( call_elements )



203
204
205
206
207
208
209
# File 'lib/opulent/parser/expression.rb', line 203

def call
  return unless (buffer = accept :round_bracket)

  buffer += call_elements
  buffer += accept_stripped :'(', :*
  buffer
end

#call_elements(buffer = '') ⇒ Object

Recursively gather expression attributes separated by a comma and add them to the expression buffer

expression1, a: expression2, expression3

Parameters:

  • buffer (String) (defaults to: '')

    Accumulator for the call elements



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/opulent/parser/expression.rb', line 218

def call_elements(buffer = '')
  # Accept both shorthand and default ruby hash style. Following DRY
  # principles, a Proc is used to assign the value to the current key
  #
  # key: value
  # :key => value
  # value
  if (symbol = accept_stripped :hash_symbol)
    buffer += symbol

    # Get the value associated to the current hash key
    if (exp = expression(true))
      buffer += exp[@value]
    else
      error :call_elements
    end

    # If there is an comma ",", recursively gather the next
    # array element into the buffer
    if (terminator = accept_stripped :comma)
      buffer += call_elements terminator
    end
  elsif (exp = expression(true))
    buffer += exp[@value]

    if (assign = accept_stripped :hash_assignment)
      buffer += assign

      # Get the value associated to the current hash key
      if (exp = expression(true))
        buffer += exp[@value]
      else
        error :call_elements
      end
    end

    # If there is an comma ",", recursively gather the next
    # array element into the buffer
    if (terminator = accept_stripped :comma)
      buffer += call_elements terminator
    end
  end

  buffer
end

#comment(parent, indent) ⇒ Object

Match one line or multiline comments

Parameters:

  • parent (Node)

    Parent node of comment

  • indent (Fixnum)

    Number of indentation characters



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
# File 'lib/opulent/parser/comment.rb', line 10

def comment(parent, indent)
  return unless accept :comment

  # Get first comment line
  buffer = accept(:line_feed)
  buffer += accept(:newline) || ''

  # Get indented comment lines
  buffer += get_indented_lines indent

  # If we have a comment which is visible in the output, we will
  # create a new comment element. Otherwise, we ignore the current
  # gathered text and we simply begin the root parsing again
  if buffer[0] == '!'
    offset = 1
    options = {}

    # Allow leading comment newline
    if buffer[1] == '^'
      offset = 2
      options[:newline] = true
    end

    parent[@children] << [
      :comment,
      buffer[offset..-1].strip,
      options,
      nil,
      indent
    ]
  end

  parent
end

#control(parent, indent) ⇒ Object

Match an if-else control structure



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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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
# File 'lib/opulent/parser/control.rb', line 7

def control(parent, indent)
  # Accept eval or multiline eval syntax and return a new node,
  return unless (structure = accept(:control))
  structure = structure.to_sym

  # Handle each and the other control structures
  condition = accept(:line_feed).strip

  # Process each control structure condition
  if structure == :each
    # Check if arguments provided correctly
    unless condition.match Tokens[:each_pattern]
      Logger.error :parse, @code, @i, @j, :each_arguments
    end

    # Conditions for each iterator
    condition = [
      Regexp.last_match[1].split(' '),
      Regexp.last_match[2].split(/,(.+)$/).map(&:strip).map(&:to_sym)
    ]

    # Array loop as default
    condition[0].unshift '{}' if condition[0].length == 1
  end

  # Else and default structures are not allowed to have any condition
  # set and the other control structures require a condition
  if structure == :else
    unless condition.empty?
      Logger.error :parse, @code, @i, @j, :condition_exists
    end
  else
    if condition.empty?
      Logger.error :parse, @code, @i, @j, :condition_missing
    end
  end

  # Add the condition and create a new child to the control parent.
  # The control parent keeps condition -> children matches for our
  # document's content
  # add_options = proc do |control_parent|
  #   control_parent.value << condition
  #   control_parent.children << []
  #
  #   root control_parent
  # end

  # If the current control structure is a parent which allows multiple
  # branches, such as an if or case, we create an array of conditions
  # which can be matched and an array of children belonging to each
  # conditional branch
  if [:if, :unless].include? structure
    # Create the control structure and get its child nodes
    control_structure = [structure, [condition], {}, [], indent]
    root control_structure, indent

    # Turn children into an array because we allow multiple branches
    control_structure[@children] = [control_structure[@children]]

    # Add it to the parent node
    parent[@children] << control_structure

  elsif structure == :case
    # Create the control structure and get its child nodes
    control_structure = [
      structure,
      [],
      { condition: condition },
      [],
      indent
    ]

    # Add it to the parent node
    parent[@children] << control_structure

  # If the control structure is a child structure, we need to find the
  # node it belongs to amont the current parent. Search from end to
  # beginning until we find the node parent
  elsif control_child structure
    # During the search, we try to find the matching parent type
    unless control_parent(structure).include? parent[@children][-1][@type]
      Logger.error :parse,
                   @code,
                   @i,
                   @j,
                   :control_child,
                   control_parent(structure)
    end

    # Gather child elements for current structure
    control_structure = [structure, [condition], {}, [], indent]
    root control_structure, indent

    # Add the new condition and children to our parent structure
    parent[@children][-1][@value] << condition
    parent[@children][-1][@children] << control_structure[@children]

  # When our control structure isn't a complex composite, we create
  # it the same way as a normal node
  else
    control_structure = [structure, condition, {}, [], indent]
    root control_structure, indent

    parent[@children] << control_structure
  end
end

#control_child(structure) ⇒ Object

Check if the current control structure requires a parent node and return the parent’s node type



117
118
119
# File 'lib/opulent/parser/control.rb', line 117

def control_child(structure)
  [:else, :elsif, :when].include? structure
end

#control_parent(structure) ⇒ Object

Check if the current control structure requires a parent node and return the parent’s node type



124
125
126
127
128
129
130
# File 'lib/opulent/parser/control.rb', line 124

def control_parent(structure)
  case structure
  when :else then [:if, :unless, :case]
  when :elsif then [:if]
  when :when then [:case]
  end
end

#define(parent, indent) ⇒ Object

Check if we match a new node definition to use within our page.

Definitions will not be recursive because, by the time we parse the definition children, the definition itself is not in the knowledgebase yet.

However, we may use previously defined nodes inside new definitions, due to the fact that they are known at parse time.

Parameters:

  • nodes (Array)

    Parent node to which we append to



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
# File 'lib/opulent/parser/define.rb', line 16

def define(parent, indent)
  return unless accept(:def)

  # Definition parent check
  Logger.error :parse, @code, @i, @j, :definition if parent[@type] != :root

  # Process data
  name = accept(:node, :*).to_sym

  # Create node
  definition = [
    :def,
    name,
    { parameters: attributes({}, true) },
    [],
    indent
  ]

  # Set definition as root node and let the parser know that we're inside
  # a definition. This is used because inside definitions we do not
  # process nodes (we do not check if they are have a definition or not).
  root definition, indent

  # Add to parent
  @definitions[name] = definition
end

#doctype(parent, indent) ⇒ Object

Match one line or multiline comments



7
8
9
10
11
12
13
# File 'lib/opulent/parser/doctype.rb', line 7

def doctype(parent, indent)
  return unless accept :doctype

  buffer = accept(:line_feed)

  parent[@children] << [:doctype, buffer.strip.to_sym, {}, nil, indent]
end

#evaluate(parent, indent) ⇒ Object

Match one line or multiline, escaped or unescaped text



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/opulent/parser/eval.rb', line 7

def evaluate(parent, indent)
  # Accept eval or multiline eval syntax and return a new node,

  return unless accept :eval

  multiline = accept(:text)

  if multiline
    # Get first evaluation line

    evaluate_code = accept(:line_feed) || ''

    # Get all the lines which are more indented than the current one

    eval_node = [:evaluate, evaluate_code.strip, {}, nil, indent]
    eval_node[@value] += accept(:newline) || ''
    eval_node[@value] += get_indented_lines(indent)
  else
    evaluate_code = accept(:line_feed) || ''
    eval_node = [:evaluate, evaluate_code.strip, {}, [], indent]

    root eval_node, indent
  end

  parent[@children] << eval_node
end

#expression(allow_assignments = true, wrapped = true, whitespace = true) ⇒ Object

Check if the parser matches an expression node



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
47
48
49
50
51
# File 'lib/opulent/parser/expression.rb', line 7

def expression(allow_assignments = true, wrapped = true, whitespace = true)
  buffer = ''

  # Build a ruby expression out of accepted literals
  while (term = (whitespace ? accept(:whitespace) : nil) ||
                modifier ||
                identifier ||
                method_call ||
                paranthesis ||
                array ||
                hash ||
                symbol ||
                percent ||
                primary_term)
    buffer += term

    # Accept operations which have a right term and raise an error if
    # we have an unfinished expression such as "a +", "b - 1 >" and other
    # expressions following the same pattern
    if wrapped && (op = operation ||
      (allow_assignments ? accept_stripped(:exp_assignment) : nil))

      buffer += op
      if (right_term = expression(allow_assignments, wrapped)).nil?
        Logger.error :parse, @code, @i, @j, :expression
      else
        buffer += right_term[@value]
      end
    elsif (op = array || op = method_call || op = ternary_operator(allow_assignments, wrapped))
      buffer += op
    end

    # Do not continue if the expression has whitespace method calls in
    # an unwrapped context because this will confuse the parser
    unless buffer.strip.empty?
      break if lookahead(:exp_identifier_lookahead).nil?
    end
  end

  if buffer.strip.empty?
    undo buffer
  else
    [:expression, buffer.strip, {}]
  end
end

#extend_attributesObject

Extend node attributes with hash from

value “value” +(paranthesis)



289
290
291
292
293
294
295
296
297
# File 'lib/opulent/parser/node.rb', line 289

def extend_attributes
  return unless accept :extend_attributes
  unescaped = accept :unescaped_value

  extension = expression(false, false, false)
  extension[@options][:escaped] = !unescaped

  extension
end

#filter(parent, indent) ⇒ Object

Check if we match an compile time filter

:filter

Parameters:

  • parent (Node)

    Parent node to which we append the element



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/opulent/parser/filter.rb', line 11

def filter(parent, indent)
  return unless (filter_name = accept :filter)

  # Get element attributes
  atts = attributes(shorthand_attributes) || {}

  # Accept inline text or multiline text feed as first child
  error :fiter unless accept(:line_feed).strip.empty?

  # Get everything under the filter and set it as the node value
  # and create a new node and set its extension
  parent[@children] << [
    :filter,
    filter_name[1..-1].to_sym,
    atts,
    get_indented_lines(indent),
    indent
  ]
end

#get_indented_lines(indent) ⇒ Object

Gather all the lines which have higher indentation than the one given as parameter and put them into the buffer

Parameters:

  • indentation (Fixnum)

    parent node strating indentation



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
147
148
# File 'lib/opulent/parser/text.rb', line 99

def get_indented_lines(indent)
  buffer = ''

  # Gather multiple blank lines between lines of text
  blank_lines = proc do
    while lookahead_next_line :line_whitespace
      @line = @code[(@i += 1)]
      @offset = 0

      buffer += accept :line_whitespace
    end
  end

  # Get blank lines until we match something
  blank_lines[]

  # Get the next indentation after the parent line
  # and set it as primary indent
  first_indent = (lookahead_next_line(:indent).to_s || '').size
  next_indent = first_indent

  # While the indentation is smaller, add the line feed  to our buffer
  while next_indent > indent
    # Advance current line and reset offset
    @line = @code[(@i += 1)]
    @offset = 0

    # Get leading whitespace trimmed with first_indent's size
    next_line_indent = accept(:indent)[first_indent..-1] || ''
    next_line_indent = next_line_indent.size

    # Add next line feed, prepend the indent and append the newline
    buffer += ' ' * next_line_indent if next_line_indent > 0
    buffer += accept_stripped(:line_feed) || ''
    buffer += accept(:newline) || ''

    # Get blank lines until we match something
    blank_lines[]

    # Check the indentation on the following line. When we reach EOF,
    # set the indentation to 0 and cause the loop to stop
    if (next_indent = lookahead_next_line :indent)
      next_indent = next_indent[0].size
    else
      next_indent = 0
    end
  end

  buffer
end

#hashObject

Check if it’s possible to parse a ruby hash literal. First, try to see if the next sequence is a hash_open token: “and if it is, then a hash_close: “” token is required next

{ hash_elements }



105
106
107
108
109
110
111
112
# File 'lib/opulent/parser/expression.rb', line 105

def hash
  return unless (buffer = accept :curly_bracket)
  accept_newline
  buffer += hash_elements
  accept_newline
  buffer += accept :'{', :*
  buffer
end

#hash_elements(buffer = '') ⇒ Object

Recursively gather expression attributions separated by a comma and add them to the expression buffer

key1: experssion1, key2 => experssion2, :key3 => experssion3

Parameters:

  • buffer (String) (defaults to: '')

    Accumulator for the hash elements



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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/opulent/parser/expression.rb', line 121

def hash_elements(buffer = '')
  value = proc do
    # Get the value associated to the current hash key
    if (exp = expression)
      buffer += exp[@value]
    else
      error :hash_elements
    end

    # If there is an hash_terminator ",", recursively gather the next
    # array element into the buffer
    if (terminator = accept_stripped :comma)
      accept_newline
      buffer += hash_elements terminator
    end
  end

  # Accept both shorthand and default ruby hash style. Following DRY
  # principles, a Proc is used to assign the value to the current key
  #
  # key:
  # :key =>
  if (symbol = accept_stripped :hash_symbol)
    buffer += symbol
    value[]
  elsif (exp = expression false)
    buffer += exp[@value]
    if (assign = accept_stripped :hash_assignment)
      buffer += assign
      value[]
    else
      error :hash_assignment
    end
  end

  # Array ended prematurely with a trailing comma, therefore the current
  # parsing process will stop
  error :hash_elements_terminator if buffer.strip[-1] == ','

  buffer
end

#html_text(parent, indent) ⇒ Object

Match one line or multiline, escaped or unescaped text



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/opulent/parser/text.rb', line 71

def html_text(parent, indent)
  return unless (text_feed = accept :html_text)

  text_node = [
    :plain,
    :text,
    {
      value: text_feed.strip,
      escaped: false
    },
    nil,
    indent
  ]

  parent[@children] << text_node
end

#identifierObject

Accept a ruby identifier such as a class, module, method or variable



165
166
167
168
169
170
171
# File 'lib/opulent/parser/expression.rb', line 165

def identifier
  return unless (buffer = accept :exp_identifier)
  if (args = call)
    buffer += args
  end
  buffer
end

#include_file(_parent, indent) ⇒ Object

Check if we have an include node, which will include a new file inside of the current one to be parsed

Parameters:

  • parent (Array)

    Parent node to which we append to



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
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/opulent/parser/include.rb', line 10

def include_file(_parent, indent)
  return unless accept :include

  # Process data
  name = accept :line_feed || ''
  name.strip!

  # Check if there is any string after the include input
  Logger.error :parse, @code, @i, @j, :include_end if name.empty?

  # Get the complete file path based on the current file being compiled
  include_path = File.expand_path name, File.dirname(@file[-1][0])

  # Try to see if it has any existing extension, otherwise add .op
  include_path += Settings::FILE_EXTENSION if File.extname(name).empty?

  # Throw an error if the file doesn't exist
  unless Dir[include_path].any?
    Logger.error :parse, @code, @i, @j, :include, name
  end

  # include entire directory tree
  Dir[include_path].each do |file|
    # Skip current file when including from same directory
    next if file == @file[-1][0]

    @file << [include_path, indent]

    # Throw an error if the file doesn't exist
    if File.directory? file
      Logger.error :parse, @code, @i, @j, :include_dir, file
    end

    # Throw an error if the file doesn't exist
    unless File.file? file
      Logger.error :parse, @code, @i, @j, :include, file
    end

    # Indent all lines and prepare them for the parser
    lines = indent_lines File.read(file), ' ' * indent
    lines << ' '

    # Indent all the output lines with the current indentation
    @code.insert @i + 1, *lines.lines
  end

  true
end

#indent_lines(text, indent) ⇒ Object

Indent all lines of the input text using give indentation

Parameters:

  • text (String)

    Input text to be indented

  • indent (String)

    Indentation string to be appended



210
211
212
213
# File 'lib/opulent/parser.rb', line 210

def indent_lines(text, indent)
  text ||= ''
  text.lines.map { |line| indent + line }.join
end

#lookahead(token) ⇒ Object

Check if the lookahead matches the chosen regular expression

Parameters:

  • token (RegEx)

    Token to be checked by the parser



166
167
168
169
170
171
# File 'lib/opulent/parser.rb', line 166

def lookahead(token)
  return nil unless @line

  # Check if we match the token to the current line.
  @line[@offset..-1].match Tokens[token]
end

#lookahead_next_line(token) ⇒ Object

Check if the lookahead matches the chosen regular expression on the following line which needs to be parsed

Parameters:

  • token (RegEx)

    Token to be checked by the parser



178
179
180
181
182
183
# File 'lib/opulent/parser.rb', line 178

def lookahead_next_line(token)
  return nil unless @code[@i + 1]

  # Check if we match the token to the current line.
  @code[@i + 1].match Tokens[token]
end

#method_callObject

Accept a ruby method call modifier



185
186
187
188
189
190
191
192
193
194
195
# File 'lib/opulent/parser/expression.rb', line 185

def method_call
  method_code = ''

  while (method_start = accept(:exp_method_call))
    method_code += method_start
    argument = call
    method_code += argument if argument
  end

  method_code == '' ? nil : method_code
end

#modifierObject

Accept a ruby module, method or context modifier

Module

@, @@, $



285
286
287
# File 'lib/opulent/parser/expression.rb', line 285

def modifier
  accept(:exp_context) || accept(:exp_module)
end

#node(parent, indent = nil) ⇒ Object

Check if we match an node node with its attributes and possibly inline text

node [ attributes ] Inline text

Parameters:

  • parent (Node)

    Parent node to which we append the node



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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/opulent/parser/node.rb', line 12

def node(parent, indent = nil)
  return unless (name = lookahead(:node_lookahead) ||
                        lookahead(:shorthand_lookahead))

  # Skip node if it's a reserved keyword
  return nil if KEYWORDS.include? name[0].to_sym

  # Accept either explicit node_name or implicit :div node_name
  # with shorthand attributes
  if (node_name = accept :node)
    node_name = node_name.to_sym
    shorthand = shorthand_attributes
  elsif (shorthand = shorthand_attributes)
    node_name = :div
  end

  # Node creation options
  options = {}

  # Get leading whitespace
  options[:recursive] = accept(:recursive)

  # Get leading whitespace
  options[:leading_whitespace] = accept_stripped(:leading_whitespace)

  # Get trailing whitespace
  options[:trailing_whitespace] = accept_stripped(:trailing_whitespace)

  # Get wrapped node attributes
  atts = attributes(shorthand) || {}

  # Inherit attributes from definition
  options[:extension] = extend_attributes

  # Get unwrapped node attributes
  options[:attributes] = attributes_assignments atts, false

  # Create node
  current_node = [:node, node_name, options, [], indent]

  # Check for self enclosing tags and definitions
  def_check = !@definitions.keys.include?(node_name) &&
              Settings::SELF_ENCLOSING.include?(node_name)

  # Check if the node is explicitly self enclosing
  if (close = accept_stripped :self_enclosing) || def_check
    current_node[@options][:self_enclosing] = true

    unless close.nil? || close.strip.empty?
      undo close
      Logger.error :parse, @code, @i, @j, :self_enclosing
    end
  end

  # Check whether we have explicit inline elements and add them
  # with increased base indentation
  if accept :inline_child
    # Inline node element
    Logger.error :parse,
                 @code,
                 @i,
                 @j,
                 :inline_child unless node current_node, indent
  elsif comment current_node, indent
    # Accept same line comments
  else
    # Accept inline text element
    text current_node, indent, false
  end

  # Add the current node to the root
  root current_node, indent

  # Add the parsed node to the parent
  parent[@children] << current_node
end

#operationObject

Accept an operation between two or more expressions



336
337
338
# File 'lib/opulent/parser/expression.rb', line 336

def operation
  accept(:exp_operation)
end

#paranthesisObject

Check if it’s possible to parse a ruby paranthesis expression wrapper.



175
176
177
178
179
180
# File 'lib/opulent/parser/expression.rb', line 175

def paranthesis
  return unless (buffer = accept :round_bracket)
  buffer += expression[@value]
  buffer += accept_stripped :'(', :*
  buffer
end

#parse(code) ⇒ Object

Initialize the parsing process by splitting the code into lines and instantiationg parser variables with their default values

Parameters:

  • code (String)

    Opulent code that needs to be analyzed

Returns:

  • Nodes array



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/opulent/parser.rb', line 56

def parse(code)
  # Split the code into lines and parse them one by one
  @code = code.lines.to_a

  # Current line index
  @i = -1

  # Current character index
  @j = 0

  # Initialize root node
  @root = [:root, nil, {}, [], -1]

  # Get all nodes starting from the root element and return output
  # nodes and definitions
  root @root

  # Check whether nodes inside definitions have a custom definition
  @definitions.each do |name, node|
    @current_def = name
    apply_definitions node
  end
  @current_def = nil

  # Check whether nodes have a custom definition
  apply_definitions @root

  # Return root element
  [@root, @definitions]
end

#percentObject

Accept a ruby percentage operator for arrays of strings, symbols and simple escaped strings

%w(word1 word2 word3)



294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/opulent/parser/expression.rb', line 294

def percent
  return unless (buffer = accept_stripped :exp_percent)
  match_start = buffer[-1]
  match_name = :"percent#{match_start}"

  unless Tokens[match_name]
    match_end = Tokens.bracket(match_start) || match_start

    match_inner = "\\#{match_start}"
    match_inner += "\\#{match_end}" unless match_end == match_start

    pattern = /(((?:[^#{match_inner}\\]|\\.)*?)#{'\\' + match_end})/

    Tokens[match_name] = pattern
  end

  buffer += accept match_name
  buffer
end

#primary_termObject

Accept any primary term and return it without the leading whitespace to the expression buffer

“string” 123 123.456 nil true false /.*/



325
326
327
328
329
330
331
332
# File 'lib/opulent/parser/expression.rb', line 325

def primary_term
  accept_stripped(:exp_string) ||
    accept_stripped(:exp_fixnum) ||
    accept_stripped(:exp_double) ||
    accept_stripped(:exp_nil) ||
    accept_stripped(:exp_regex) ||
    accept_stripped(:exp_boolean)
end

#root(parent = @root, min_indent = -1)) ⇒ Object

Analyze the input code and check for matching tokens. In case no match was found, throw an exception. In special cases, modify the token hash.

Parameters:

  • parent (Array) (defaults to: @root)

    Parent node to which we append to



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
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/opulent/parser/root.rb', line 11

def root(parent = @root, min_indent = -1)
  while (@line = @code[(@i += 1)])
    # Reset character position cursor
    @j = 0

    # Skip to next iteration if we have a blank line
    next if @line =~ /\A\s*\Z/

    # Reset the line offset
    @offset = 0

    # Parse the current line by trying to match each node type towards it
    # Add current indentation to the indent stack
    indent = accept(:indent).size

    # Stop using the current parent as root if it does not match the
    # minimum indentation includements
    unless min_indent < indent
      @i -= 1
      break
    end

    # If last include path had a greater indentation, pop the last file path
    @file.pop if @file[-1][1] >= indent

    # Try the main Opulent node types and process each one of them using
    # their matching evaluation procedure
    current_node = node(parent, indent) ||
                   text(parent, indent) ||
                   comment(parent, indent) ||
                   define(parent, indent) ||
                   control(parent, indent) ||
                   evaluate(parent, indent) ||
                   filter(parent, indent) ||
                   block_yield(parent, indent) ||
                   include_file(parent, indent) ||
                   html_text(parent, indent) ||
                   doctype(parent, indent)

    # Throw an error if we couldn't find any valid node
    unless current_node
      Logger.error :parse, @code, @i, @j, :unknown_node_type
    end
  end

  parent
end

#shorthand_attributes(atts = {}) ⇒ Object

Accept node shorthand attributes. Each shorthand attribute is directly mapped to an attribute key

Parameters:

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

    Node attributes



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/opulent/parser/node.rb', line 164

def shorthand_attributes(atts = {})
  while (key = accept :shorthand)
    key = Settings::SHORTHAND[key.to_sym]

    # Check whether the value is escaped or unescaped
    escaped = accept(:unescaped_value) ? false : true

    # Get the attribute value and process it
    if (value = accept(:shorthand_node))
      value = [:expression, value.inspect, { escaped: escaped }]
    elsif (value = accept(:exp_string))
      value = [:expression, value, { escaped: escaped }]
    elsif (value = paranthesis)
      value = [:expression, value, { escaped: escaped }]
    else
      Logger.error :parse, @code, @i, @j, :shorthand
    end

    # IDs are unique, the rest of the attributes turn into arrays in
    # order to allow multiple values or identifiers
    add_attribute(atts, key, value)
  end

  atts
end

#symbolObject

Accept a ruby symbol defined through a colon and a trailing expression

:‘symbol’ :symbol



269
270
271
272
273
274
275
276
277
278
# File 'lib/opulent/parser/expression.rb', line 269

def symbol
  return unless (colon = accept :colon)
  return undo colon if lookahead(:whitespace)

  if (exp = expression).nil?
    error :symbol
  else
    colon + exp[@value]
  end
end

#ternary_operator(allow_assignments, wrapped) ⇒ Object

Accept ternary operator syntax

condition ? expression1 : expression2



344
345
346
347
348
349
350
351
352
353
# File 'lib/opulent/parser/expression.rb', line 344

def ternary_operator(allow_assignments, wrapped)
  if (buffer = accept :exp_ternary)
    buffer += expression(allow_assignments, wrapped)[@value]
    if (else_branch = accept :exp_ternary_else)
      buffer += else_branch
      buffer += expression(allow_assignments, wrapped)[@value]
    end
    return buffer
  end
end

#text(parent, indent = nil, multiline_or_print = true) ⇒ Object

Match one line or multiline, escaped or unescaped text

Parameters:

  • parent (Array)

    Parent node element

  • indent (Fixnum) (defaults to: nil)

    Size of the current indentation

  • multiline_or_print (Boolean) (defaults to: true)

    Allow only multiline text or print



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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/opulent/parser/text.rb', line 11

def text(parent, indent = nil, multiline_or_print = true)
  # Try to see if we can match a multiline operator. If we can accept_stripped only
  # multiline, which is the case for filters, undo the operation.
  if accept :multiline
    multiline = true
  elsif multiline_or_print
    return nil unless lookahead :print_lookahead
  end

  # Get text node type
  type = accept(:print) ? :print : :text

  # Get leading whitespace
  leading_whitespace = accept(:leading_whitespace)

  # Get trailing whitespace
  trailing_whitespace = accept(:trailing_whitespace)

  # Check if the text or print node is escaped or unescaped
  escaped = accept(:unescaped_value) ? false : true

  # Get text value
  value = accept :line_feed
  value = value[1..-1] if value[0] == '\\'

  # Create the text node using input data
  text_node = [
    :plain,
    type,
    {
      value: value.strip,
      escaped: escaped,
      leading_whitespace: leading_whitespace,
      trailing_whitespace: trailing_whitespace
    },
    nil,
    indent
  ]

  # If we have a multiline node, get all the text which has higher
  # indentation than our indentation node.
  if multiline
    text_node[@options][:value] += accept(:newline) || ''
    text_node[@options][:value] += get_indented_lines(indent)
    text_node[@options][:value].strip!
  elsif value.empty?
    # If our value is empty and we're not going to add any more lines to
    # our buffer, skip the node
    return nil
  end

  # Increase indentation if this is an inline text node
  text_node[@indent] += @settings[:indent] unless multiline_or_print

  # Add text node to the parent element
  parent[@children] << text_node
end

#undo(match) ⇒ Object

Undo a found match by removing the token from the consumed code and adding it back to the code chunk

Parameters:

  • match (String)

    Matched string to be undone



190
191
192
193
194
# File 'lib/opulent/parser.rb', line 190

def undo(match)
  return if match.empty?
  @offset -= match.size
  nil
end

#whitespace(required = false) ⇒ Object

Match a whitespace by preventing code trimming



90
91
92
# File 'lib/opulent/parser/text.rb', line 90

def whitespace(required = false)
  accept :whitespace, required
end

#wrapped_attributes(list = {}, for_definition = false) ⇒ Object

Check if we match node attributes

assignments

Parameters:

  • as_parameters (Boolean)

    Accept or reject identifier nodes



207
208
209
210
211
212
213
214
215
216
217
# File 'lib/opulent/parser/node.rb', line 207

def wrapped_attributes(list = {}, for_definition = false)
  return unless (bracket = accept :brackets)

  accept_newline
  attributes_assignments list, true, for_definition
  accept_newline

  accept_stripped bracket.to_sym, :*

  list
end