Class: Papercraft::Compiler

Inherits:
Sirop::Sourcifier
  • Object
show all
Defined in:
lib/papercraft/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/papercraft/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/papercraft/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 = Papercraft::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/papercraft/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/papercraft/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/papercraft/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/papercraft/compiler.rb', line 70

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

.source_map_storeObject



59
60
61
# File 'lib/papercraft/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/papercraft/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/papercraft/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/papercraft/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



409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# File 'lib/papercraft/compiler.rb', line 409

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:



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/papercraft/compiler.rb', line 314

def visit_builtin_node(node)
  case node.tag
  when :tag
    args = node.call_node.arguments&.arguments
  when :html, :html5
    emit_html(node.location, '<!DOCTYPE html>')
    emit_html(node.location, format_html_tag_open(node.location, 'html', node.attributes))
    # 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("Papercraft.markdown(#{format_code(args)})"))
  end
end

#visit_const_tag_node(node) ⇒ void

This method returns an undefined value.

Visits a const tag node.

Parameters:



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/papercraft/compiler.rb', line 208

def visit_const_tag_node(node)
  flush_html_parts!
  adjust_whitespace(node.location)
  emit("; ")
  if node.call_node.receiver
    emit(format_code(node.call_node.receiver))
    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:



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/papercraft/compiler.rb', line 290

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:



336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/papercraft/compiler.rb', line 336

def visit_extension_tag_node(node)
  flush_html_parts!
  adjust_whitespace(node.location)
  emit("; Papercraft::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 Papercraft::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:



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/papercraft/compiler.rb', line 270

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 (Papercraft::RenderChildrenNode)

    node



397
398
399
400
401
402
403
404
405
406
407
# File 'lib/papercraft/compiler.rb', line 397

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:



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/papercraft/compiler.rb', line 228

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 (Papercraft::RenderYieldNode)

    node



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

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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/papercraft/compiler.rb', line 160

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

  # adjust_whitespace(node.location)
  is_void = is_void_element?(tag)
  is_raw_inner_text = is_raw_inner_text_element?(tag)

  if is_void && (node.block || node.inner_text)
    raise Papercraft::Error, "Void element #{tag} cannot contain child nodes or inner text"
  end

  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)
      if is_raw_inner_text
        emit_html(node.location, format_literal(node.inner_text))
      else
        emit_html(node.location, ERB::Escape.html_escape(format_literal(node.inner_text)))
      end
    else
      if is_raw_inner_text
        emit_html(node.location, interpolated(format_code(node.inner_text)))
      else
        emit_html(node.location, interpolated("ERB::Escape.html_escape((#{format_code(node.inner_text)}))"))
      end
    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:



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/papercraft/compiler.rb', line 250

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/papercraft/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