Class: P2::Compiler

Inherits:
Sirop::Sourcifier
  • Object
show all
Defined in:
lib/p2/compiler.rb

Overview

A Compiler converts a template into an optimized form that generates HTML efficiently.

Constant Summary collapse

@@html_debug_attribute_injector =
nil

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(mode:) ⇒ Compiler

Initializes a compiler.



77
78
79
80
81
82
# File 'lib/p2/compiler.rb', line 77

def initialize(mode:, **)
  super(**)
  @mode = mode
  @pending_html_parts = []
  @level = 0
end

Instance Attribute Details

#source_mapObject (readonly)

Returns the value of attribute source_map.



74
75
76
# File 'lib/p2/compiler.rb', line 74

def source_map
  @source_map
end

Class Method Details

.compile(proc, mode: :html, wrap: true) ⇒ Proc

Compiles the given template into an optimized Proc that generates HTML.

template = -> {
  h1 'Hello, world!'
}
compiled = P2::Compiler.compile(template)
compiled.render #=> '<h1>Hello, world!'

Parameters:

  • proc (Proc)

    template

  • mode (Symbol) (defaults to: :html)

    compilation mode (:html, :xml)

  • wrap (bool) (defaults to: true)

    whether to wrap the generated code with a literal Proc definition

Returns:

  • (Proc)

    compiled proc



50
51
52
53
54
55
56
57
# File 'lib/p2/compiler.rb', line 50

def self.compile(proc, mode: :html, wrap: true)
  source_map, code = compile_to_code(proc, mode:, wrap:)
  if ENV['DEBUG'] == '1'
    puts '*' * 40
    puts code
  end
  eval(code, proc.binding, source_map[:compiled_fn])
end

.compile_to_code(proc, mode: :html, wrap: true) ⇒ Array

Compiles the given proc, returning the generated source map and the generated optimized source code.

Parameters:

  • proc (Proc)

    template

  • mode (Symbol) (defaults to: :html)

    compilation mode (:html, :xml)

  • wrap (bool) (defaults to: true)

    whether to wrap the generated code with a literal Proc definition

Returns:

  • (Array)

    array containing the source map and generated code



26
27
28
29
30
31
32
33
34
35
36
# File 'lib/p2/compiler.rb', line 26

def self.compile_to_code(proc, mode: :html, wrap: true)
  ast = Sirop.to_ast(proc)

  # adjust ast root if proc is defined with proc {} / lambda {} syntax
  ast = ast.block if ast.is_a?(Prism::CallNode)

  compiler = new(mode:).with_source_map(proc, ast)
  transformed_ast = TagTranslator.transform(ast.body, ast)
  compiler.format_compiled_template(transformed_ast, ast, wrap:, binding: proc.binding)
  [compiler.source_map, compiler.buffer]
end

.html_debug_attribute_injector=(proc) ⇒ Object



15
16
17
# File 'lib/p2/compiler.rb', line 15

def self.html_debug_attribute_injector=(proc)
  @@html_debug_attribute_injector = proc
end

.source_location_to_fn(source_location) ⇒ Object



70
71
72
# File 'lib/p2/compiler.rb', line 70

def self.source_location_to_fn(source_location)
  "::(#{source_location.join(':')})"
end

.source_map_storeObject



59
60
61
# File 'lib/p2/compiler.rb', line 59

def self.source_map_store
  @source__map_store ||= {}
end

.store_source_map(source_map) ⇒ Object



63
64
65
66
67
68
# File 'lib/p2/compiler.rb', line 63

def self.store_source_map(source_map)
  return if !source_map

  fn = source_map[:compiled_fn]
  source_map_store[fn] = source_map
end

Instance Method Details

#format_compiled_template(ast, orig_ast, wrap:, binding:) ⇒ String

Formats the source code for a compiled template proc.

Parameters:

  • ast (Prism::Node)

    translated AST

  • orig_ast (Prism::Node)

    original template AST

  • wrap (bool)

    whether to wrap the generated code with a literal Proc definition

Returns:

  • (String)

    compiled template source code



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
149
150
151
152
153
154
# File 'lib/p2/compiler.rb', line 116

def format_compiled_template(ast, orig_ast, wrap:, binding:)
  # generate source code
  @binding = binding
  update_source_map
  visit(ast)
  flush_html_parts!(semicolon_prefix: true)
  update_source_map

  source_code = @buffer
  @buffer = +''
  if wrap
    @source_map[2] = "#{@orig_proc_fn}:#{loc_start(orig_ast.location).first}"
    emit("# frozen_string_literal: true\n->(__buffer__")

    params = orig_ast.parameters
    params = params&.parameters
    if params
      emit(', ')
      emit(format_code(params))
    end

    if @render_yield_used || @render_children_used
      emit(', &__block__')
    end

    emit(") {\n")

  end
  @buffer << source_code
  emit_defer_postlude if @defer_mode
  if wrap
    emit('; __buffer__')
    adjust_whitespace(orig_ast.closing_loc)
    emit('}')
  end
  update_source_map
  Compiler.store_source_map(@source_map)
  @buffer
end

#update_source_map(str = nil) ⇒ Object



101
102
103
104
105
106
107
108
# File 'lib/p2/compiler.rb', line 101

def update_source_map(str = nil)
  return if !@source_map

  buffer_cur_line = @buffer.count("\n") + 1
  orig_source_cur_line = @last_loc_start ? @last_loc_start.first : '?'
  @source_map[buffer_cur_line + @source_map_line_ofs] ||=
    "#{@orig_proc_fn}:#{orig_source_cur_line}"
end

#visit_block_invocation_node(node) ⇒ Object



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/p2/compiler.rb', line 392

def visit_block_invocation_node(node)
  flush_html_parts!
  adjust_whitespace(node.location)

  emit("; #{node.call_node.receiver.name}.compiled_proc.(__buffer__")
  if node.call_node.arguments
    emit(', ')
    visit(node.call_node.arguments)
  end
  if node.call_node.block
    emit(", &(->")
    visit(node.call_node.block)
    emit(").compiled_proc")
  end
  emit(")")
end

#visit_builtin_node(node) ⇒ void

This method returns an undefined value.

Visits a builtin node.

Parameters:



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/p2/compiler.rb', line 299

def visit_builtin_node(node)
  case node.tag
  when :tag
    args = node.call_node.arguments&.arguments
  when :html5
    emit_html(node.location, '<!DOCTYPE html><html>')
    visit(node.block.body) if node.block
    emit_html(node.block.closing_loc, '</html>')
  when :markdown
    args = node.call_node.arguments
    return if !args

    emit_html(node.location, interpolated("P2.markdown(#{format_code(args)})"))
  end
end

#visit_const_tag_node(node) ⇒ void

This method returns an undefined value.

Visits a const tag node.

Parameters:



194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/p2/compiler.rb', line 194

def visit_const_tag_node(node)
  flush_html_parts!
  adjust_whitespace(node.location)
  if node.call_node.receiver
    emit(node.call_node.receiver.location)
    emit('::')
  end
  emit("; #{node.call_node.name}.compiled_proc.(__buffer__")
  if node.call_node.arguments
    emit(', ')
    visit(node.call_node.arguments)
  end
  emit(');')
end

#visit_defer_node(node) ⇒ void

This method returns an undefined value.

Visits a defer node.

Parameters:



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/p2/compiler.rb', line 275

def visit_defer_node(node)
  block = node.block
  return if !block

  flush_html_parts!

  if !@defer_mode
    adjust_whitespace(node.call_node.message_loc)
    emit("__orig_buffer__ = __buffer__; __parts__ = __buffer__ = []; ")
    @defer_mode = true
  end

  adjust_whitespace(block.opening_loc)
  emit("__buffer__ << ->{")
  visit(block.body)
  flush_html_parts!
  adjust_whitespace(block.closing_loc)
  emit("}")
end

#visit_extension_tag_node(node) ⇒ void

This method returns an undefined value.

Visits a extension tag node.

Parameters:



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/p2/compiler.rb', line 319

def visit_extension_tag_node(node)
  flush_html_parts!
  adjust_whitespace(node.location)
  emit("; P2::Extensions[#{node.tag.inspect}].compiled_proc.(__buffer__")
  if node.call_node.arguments
    emit(', ')
    visit(node.call_node.arguments)
  end
  if node.block
    block_body = format_inline_block(node.block.body)
    block_params = []

    if node.block.parameters.is_a?(Prism::ItParametersNode)
      raise P2::Error, "Blocks passed to extensions cannot use it parameter"
    end

    if (params = node.block.parameters&.parameters)
      params.requireds.each do
        block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
      end
      params.optionals.each do
        block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
      end
      block_params << format_code(params.rest) if params.rest
      params.posts.each do
        block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
      end
      params.keywords.each do
        block_params << format_code(it) if !it.is_a?(Prism::ItParametersNode)
      end
      block_params << format_code(params.keyword_rest) if params.keyword_rest
    end
    block_params = block_params.empty? ? '' : ", #{block_params.join(', ')}"

    emit(", &(proc { |__buffer__#{block_params}| #{block_body} }).compiled!")
  end
  emit(")")
end

#visit_raw_node(node) ⇒ void

This method returns an undefined value.

Visits a raw node.

Parameters:



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/p2/compiler.rb', line 255

def visit_raw_node(node)
  return if !node.call_node.arguments

  args = node.call_node.arguments.arguments
  first_arg = args.first
  if args.length == 1
    if is_static_node?(first_arg)
      emit_html(node.location, format_literal(first_arg))
    else
      emit_html(node.location, interpolated("(#{format_code(first_arg)}).to_s"))
    end
  else
    raise "Don't know how to compile #{node}"
  end
end

#visit_render_children_node(node) ⇒ void

This method returns an undefined value.

Visits a render_children node.

Parameters:

  • node (P2::RenderChildrenNode)

    node



380
381
382
383
384
385
386
387
388
389
390
# File 'lib/p2/compiler.rb', line 380

def visit_render_children_node(node)
  flush_html_parts!
  adjust_whitespace(node.location)
  @render_children_used = true
  emit("; __block__&.compiled_proc&.(__buffer__")
  if node.call_node.arguments
    emit(', ')
    visit(node.call_node.arguments)
  end
  emit(")")
end

#visit_render_node(node) ⇒ void

This method returns an undefined value.

Visits a render node.

Parameters:



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/p2/compiler.rb', line 213

def visit_render_node(node)
  args = node.call_node.arguments.arguments
  first_arg = args.first

  block_embed = node.block && "&(->(__buffer__) #{format_code(node.block)}.compiled!)"
  block_embed = ", #{block_embed}" if block_embed && node.call_node.arguments

  flush_html_parts!
  adjust_whitespace(node.location)

  if args.length == 1
    emit("; #{format_code(first_arg)}.compiled_proc.(__buffer__#{block_embed})")
  else
    args_code = format_code_comma_separated_nodes(args[1..])
    emit("; #{format_code(first_arg)}.compiled_proc.(__buffer__, #{args_code}#{block_embed})")
  end
end

#visit_render_yield_node(node) ⇒ void

This method returns an undefined value.

Visits a render_yield node.

Parameters:

  • node (P2::RenderYieldNode)

    node



362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/p2/compiler.rb', line 362

def visit_render_yield_node(node)
  flush_html_parts!
  adjust_whitespace(node.location)
  guard = @render_yield_used ?
    '' : "; raise(LocalJumpError, 'no block given (render_yield)') if !__block__"
  @render_yield_used = true
  emit("#{guard}; __block__.compiled_proc.(__buffer__")
  if node.call_node.arguments
    emit(', ')
    visit(node.call_node.arguments)
  end
  emit(")")
end

#visit_tag_node(node) ⇒ void

This method returns an undefined value.

Visits a tag node.

Parameters:



160
161
162
163
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/p2/compiler.rb', line 160

def visit_tag_node(node)
  @level += 1
  tag = node.tag

  # adjust_whitespace(node.location)
  is_void = is_void_element?(tag)
  emit_html(node.tag_location, format_html_tag_open(node.tag_location, tag, node.attributes))
  return if is_void

  case node.block
  when Prism::BlockNode
    visit(node.block.body)
  when Prism::BlockArgumentNode
    flush_html_parts!
    adjust_whitespace(node.block)
    emit("; #{format_code(node.block.expression)}.compiled_proc.(__buffer__)")
  end

  if node.inner_text
    if is_static_node?(node.inner_text)
      emit_html(node.location, ERB::Escape.html_escape(format_literal(node.inner_text)))
    else
      emit_html(node.location, interpolated("ERB::Escape.html_escape((#{format_code(node.inner_text)}))"))
    end
  end
  emit_html(node.location, format_html_tag_close(tag))
ensure
  @level -= 1
end

#visit_text_node(node) ⇒ void

This method returns an undefined value.

Visits a text node.

Parameters:



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/p2/compiler.rb', line 235

def visit_text_node(node)
  return if !node.call_node.arguments

  args = node.call_node.arguments.arguments
  first_arg = args.first
  if args.length == 1
    if is_static_node?(first_arg)
      emit_html(node.location, ERB::Escape.html_escape(format_literal(first_arg)))
    else
      emit_html(node.location, interpolated("ERB::Escape.html_escape(#{format_code(first_arg)})"))
    end
  else
    raise "Don't know how to compile #{node}"
  end
end

#with_source_map(orig_proc, orig_ast) ⇒ self

Initializes a source map.

Parameters:

  • orig_proc (Proc)

    template proc

  • orig_ast (Prism::Node)

    template AST

Returns:

  • (self)


89
90
91
92
93
94
95
96
97
98
99
# File 'lib/p2/compiler.rb', line 89

def with_source_map(orig_proc, orig_ast)
  @fn = orig_proc.source_location.first
  @orig_proc = orig_proc
  @orig_proc_fn = orig_proc.source_location.first
  @source_map = {
    source_fn: orig_proc.source_location.first,
    compiled_fn: Compiler.source_location_to_fn(orig_proc.source_location)
  }
  @source_map_line_ofs = 2
  self
end