class Papercraft::Compiler

The Compiler class compiles Papercraft templates

Constants

DEFAULT_CODE_BUFFER_CAPACITY
DEFAULT_EMIT_BUFFER_CAPACITY

Attributes

code_buffer[R]

Public Class Methods

new() click to toggle source
# File lib/papercraft/compiler.rb, line 9
def initialize
  @level = 1
  @code_buffer = String.new(capacity: DEFAULT_CODE_BUFFER_CAPACITY)
end
pp_ast(node, level = 0) click to toggle source
# File lib/papercraft/compiler.rb, line 413
def self.pp_ast(node, level = 0)
  case node
  when RubyVM::AbstractSyntaxTree::Node
    puts "#{'  ' * level}#{node.type.inspect}"
    node.children.each { |c| pp_ast(c, level + 1) }
  when Array
    puts "#{'  ' * level}["
    node.each { |c| pp_ast(c, level + 1) }
    puts "#{'  ' * level}]"
  else
    puts "#{'  ' * level}#{node.inspect}"
    return
  end
end

Public Instance Methods

compile(template) click to toggle source
# File lib/papercraft/compiler.rb, line 85
def compile(template)
  @block = template.to_proc
  ast = RubyVM::AbstractSyntaxTree.of(@block)
  # Compiler.pp_ast(ast)
  parse(ast)
  flush_emit_buffer
  self
end
emit_code(code) click to toggle source
# File lib/papercraft/compiler.rb, line 72
def emit_code(code)
  if flush_emit_buffer || @line_break
    emit_code_line_break if @line_break
    @code_buffer << "#{'  ' * @level}#{code}"
  else
    if @code_buffer.empty? || (@code_buffer[-1] == "\n")
      @code_buffer << "#{'  ' * @level}#{code}"
    else
      @code_buffer << "#{code}"
    end
  end
end
emit_code_line_break() click to toggle source
# File lib/papercraft/compiler.rb, line 20
def emit_code_line_break
  return if @code_buffer.empty?

  @code_buffer << "\n" if @code_buffer[-1] != "\n"
  @line_break = nil
end
emit_expression() { || ... } click to toggle source
# File lib/papercraft/compiler.rb, line 54
def emit_expression
  if @output_mode
    emit_literal('#{__html_encode__(')
    yield
    emit_literal(')}')
  else
    yield
  end
end
emit_if_code(cond, then_branch, else_branch) click to toggle source
# File lib/papercraft/compiler.rb, line 372
def emit_if_code(cond, then_branch, else_branch)
  emit_code('if ')
  parse(cond)
  emit_code("\n")
  @level += 1
  parse(then_branch)
  flush_emit_buffer
  @level -= 1
  if else_branch
    emit_code("else\n")
    @level += 1
    parse(else_branch)
    flush_emit_buffer
    @level -= 1
  end
  emit_code("end\n")
end
emit_if_output(cond, then_branch, else_branch) click to toggle source
# File lib/papercraft/compiler.rb, line 348
def emit_if_output(cond, then_branch, else_branch)
  parse(cond)
  emit_literal(" ? ")
  parse(then_branch)
  emit_literal(" : ")
  if else_branch
    parse(else_branch)
  else
    emit_literal(nil)
  end
end
emit_literal(lit) click to toggle source
# File lib/papercraft/compiler.rb, line 27
def emit_literal(lit)
  if @output_mode
    emit_code_line_break if @line_break
    @emit_buffer ||= String.new(capacity: DEFAULT_EMIT_BUFFER_CAPACITY)
    @emit_buffer << lit
  else
    emit_code(lit)
  end
end
emit_output() { || ... } click to toggle source
# File lib/papercraft/compiler.rb, line 14
def emit_output
  @output_mode = true
  yield
  @output_mode = false
end
emit_tag(tag, atts, &block) click to toggle source
# File lib/papercraft/compiler.rb, line 183
def emit_tag(tag, atts, &block)
  emit_output do
    if atts
      emit_literal("<#{tag}")
      emit_tag_attributes(atts)
      emit_literal(block ? '>' : '/>')
    else
      emit_literal(block ? "<#{tag}>" : "<#{tag}/>")
    end
  end
  if block
    block.call
    emit_output { emit_literal("</#{tag}>") }
  end
end
emit_tag_attribute_key(key) click to toggle source
# File lib/papercraft/compiler.rb, line 222
def emit_tag_attribute_key(key)
  case key.type
  when :STR
    emit_literal(key.children.first)
  when :LIT
    emit_literal(key.children.first.to_s)
  when :NIL
    emit_literal('nil')
  else
    emit_expression { parse(key) }
  end
end
emit_tag_attribute_value(value, key) click to toggle source
# File lib/papercraft/compiler.rb, line 235
def emit_tag_attribute_value(value, key)
  case value.type
  when :STR
    encoding = (key.type == :LIT) && (key.children.first == :href) ? :uri : :html
    emit_text(value.children.first, encoding: encoding)
  when :LIT
    emit_text(value.children.first.to_s)
  else
    parse(value)
  end
end
emit_tag_attributes(atts) click to toggle source
# File lib/papercraft/compiler.rb, line 199
def emit_tag_attributes(atts)
  list = atts.children.first.children
  while true
    key = list.shift
    break unless key

    value = list.shift
    value_type = value.type
    case value_type
    when :FALSE, :NIL
      next
    end

    emit_literal(' ')
    emit_tag_attribute_key(key)
    next if value_type == :TRUE

    emit_literal('=\"')
    emit_tag_attribute_value(value, key)
    emit_literal('\"')
  end
end
emit_text(str, encoding: :html) click to toggle source
# File lib/papercraft/compiler.rb, line 37
def emit_text(str, encoding: :html)
  emit_code_line_break if @line_break
  @emit_buffer ||= String.new(capacity: DEFAULT_EMIT_BUFFER_CAPACITY)
  @emit_buffer << encode(str, encoding).inspect[1..-2]
end
emit_unless_code(cond, then_branch, else_branch) click to toggle source
# File lib/papercraft/compiler.rb, line 390
def emit_unless_code(cond, then_branch, else_branch)
  emit_code('unless ')
  parse(cond)
  emit_code("\n")
  @level += 1
  parse(then_branch)
  flush_emit_buffer
  @level -= 1
  if else_branch
    emit_code("else\n")
    @level += 1
    parse(else_branch)
    flush_emit_buffer
    @level -= 1
  end
  emit_code("end\n")
end
emit_unless_output(cond, then_branch, else_branch) click to toggle source
# File lib/papercraft/compiler.rb, line 360
def emit_unless_output(cond, then_branch, else_branch)
  parse(cond)
  emit_literal(" ? ")
  if else_branch
    parse(else_branch)
  else
    emit_literal(nil)
  end
  emit_literal(" : ")
  parse(then_branch)
end
encode(str, encoding) click to toggle source
# File lib/papercraft/compiler.rb, line 43
def encode(str, encoding)
  case encoding
  when :html
    __html_encode__(str)
  when :uri
    __uri_encode__(str)
  else
    raise "Invalid encoding #{encoding.inspect}"
  end
end
fcall_attributes_from_args(args) click to toggle source
# File lib/papercraft/compiler.rb, line 176
def fcall_attributes_from_args(args)
  return nil if !args

  last = args.last
  (last.type == :HASH) ? last : nil
end
fcall_inner_text_from_args(args) click to toggle source
# File lib/papercraft/compiler.rb, line 160
def fcall_inner_text_from_args(args)
  return nil if !args

  first = args.first
  case first.type
  when :STR
    first.children.first
  when :LIT
    first.children.first.to_s
  when :HASH
    nil
  else
    first
  end
end
flush_emit_buffer() click to toggle source
# File lib/papercraft/compiler.rb, line 64
def flush_emit_buffer
  return if !@emit_buffer

  @code_buffer << "#{'  ' * @level}__buffer__ << \"#{@emit_buffer}\"\n"
  @emit_buffer = nil
  true
end
parse(node) click to toggle source
# File lib/papercraft/compiler.rb, line 104
def parse(node)
  @line_break = @last_node && node.first_lineno != @last_node.first_lineno
  @last_node = node
  # puts "- parse(#{node.type}) (break: #{@line_break.inspect})"
  send(:"parse_#{node.type.downcase}", node)
end
parse_block(node) click to toggle source
# File lib/papercraft/compiler.rb, line 326
def parse_block(node)
  node.children.each { |c| parse(c) }
end
parse_call(node) click to toggle source
# File lib/papercraft/compiler.rb, line 247
def parse_call(node)
  receiver, method, args = node.children
  if receiver.type == :VCALL && receiver.children == [:context]
    emit_literal('__context__')
  else
    parse(receiver)
  end
  if method == :[]
    emit_literal('[')
    args = args.children.compact
    while true
      arg = args.shift
      break unless arg

      parse(arg)
      emit_literal(', ') if !args.empty?
    end
    emit_literal(']')
  else
    emit_literal('.')
    emit_literal(method.to_s)
    if args
      emit_literal('(')
      args = args.children.compact
      while true
        arg = args.shift
        break unless arg

        parse(arg)
        emit_literal(', ') if !args.empty?
      end
      emit_literal(')')
    end
  end
end
parse_dvar(node) click to toggle source
# File lib/papercraft/compiler.rb, line 408
def parse_dvar(node)

  emit_literal(node.children.first.to_s)
end
parse_false(node) click to toggle source
# File lib/papercraft/compiler.rb, line 297
def parse_false(node)
  emit_expression { emit_literal('true') }
end
parse_fcall(node, block = nil) click to toggle source
# File lib/papercraft/compiler.rb, line 138
def parse_fcall(node, block = nil)
  tag, args = node.children
  args = args.children.compact if args
  text = fcall_inner_text_from_args(args)
  atts = fcall_attributes_from_args(args)
  if block
    emit_tag(tag, atts) { parse(block) }
  elsif text
    emit_tag(tag, atts) do
      emit_output do
        if text.is_a?(String)
          emit_text(text)
        else
          emit_expression { parse(text) }
        end
      end
    end
  else
    emit_tag(tag, atts)
  end
end
parse_if(node) click to toggle source
# File lib/papercraft/compiler.rb, line 330
def parse_if(node)
  cond, then_branch, else_branch = node.children
  if @output_mode
    emit_if_output(cond, then_branch, else_branch)
  else
    emit_if_code(cond, then_branch, else_branch)
  end
end
parse_iter(node) click to toggle source
# File lib/papercraft/compiler.rb, line 115
def parse_iter(node)
  call, scope = node.children
  if call.type == :FCALL
    parse_fcall(call, scope)
  else
    parse(call)
    emit_code(" do")
    args = scope.children[0]
    emit_code(" |#{args.join(', ')}|") if args
    emit_code("\n")
    @level += 1
    parse(scope)
    flush_emit_buffer
    @level -= 1
    emit_code("end\n")
  end
end
parse_ivar(node) click to toggle source
# File lib/papercraft/compiler.rb, line 133
def parse_ivar(node)
  ivar = node.children.first.match(/^@(.+)*/)[1]
  emit_literal("__context__[:#{ivar}]")
end
parse_list(node) click to toggle source
# File lib/papercraft/compiler.rb, line 301
def parse_list(node)
  emit_literal('[')
  items = node.children.compact
  while true
    item = items.shift
    break unless item

    parse(item)
    emit_literal(', ') if !items.empty?
  end
  emit_literal(']')
end
parse_lit(node) click to toggle source
# File lib/papercraft/compiler.rb, line 288
def parse_lit(node)
  value = node.children.first
  emit_literal(value.inspect)
end
parse_opcall(node) click to toggle source
# File lib/papercraft/compiler.rb, line 319
def parse_opcall(node)
  left, op, right = node.children
  parse(left)
  emit_literal(" #{op} ")
  right.children.compact.each { |c| parse(c) }
end
parse_scope(node) click to toggle source
# File lib/papercraft/compiler.rb, line 111
def parse_scope(node)
  parse(node.children[2])
end
parse_str(node) click to toggle source
# File lib/papercraft/compiler.rb, line 283
def parse_str(node)
  str = node.children.first
  emit_literal(str.inspect)
end
parse_true(node) click to toggle source
# File lib/papercraft/compiler.rb, line 293
def parse_true(node)
  emit_expression { emit_literal('true') }
end
parse_unless(node) click to toggle source
# File lib/papercraft/compiler.rb, line 339
def parse_unless(node)
  cond, then_branch, else_branch = node.children
  if @output_mode
    emit_unless_output(cond, then_branch, else_branch)
  else
    emit_unless_code(cond, then_branch, else_branch)
  end
end
parse_vcall(node) click to toggle source
# File lib/papercraft/compiler.rb, line 314
def parse_vcall(node)
  tag = node.children.first
  emit_tag(tag, nil)
end
to_code() click to toggle source
# File lib/papercraft/compiler.rb, line 96
def to_code
  "->(__buffer__, __context__) do\n#{@code_buffer}end"
end
to_proc() click to toggle source
# File lib/papercraft/compiler.rb, line 100
def to_proc
  @block.binding.eval(to_code)
end