class ReVIEW::Compiler

Constants

INLINE
SYNTAX

Attributes

strategy[R]

Public Class Methods

defblock(name, argc, optional = false, &block) click to toggle source
# File lib/review/compiler.rb, line 86
def self.defblock(name, argc, optional = false, &block)
  defsyntax name, (optional ? :optional : :block), argc, &block
end
definline(name) click to toggle source
# File lib/review/compiler.rb, line 98
def self.definline(name)
  INLINE[name] = InlineSyntaxElement.new(name)
end
defsingle(name, argc, &block) click to toggle source
# File lib/review/compiler.rb, line 90
def self.defsingle(name, argc, &block)
  defsyntax name, :line, argc, &block
end
defsyntax(name, type, argc, &block) click to toggle source
# File lib/review/compiler.rb, line 94
def self.defsyntax(name, type, argc, &block)
  SYNTAX[name] = SyntaxElement.new(name, type, argc, &block)
end
new(strategy) click to toggle source
# File lib/review/compiler.rb, line 39
def initialize(strategy)
  @strategy = strategy
end

Public Instance Methods

compile(chap) click to toggle source
# File lib/review/compiler.rb, line 45
def compile(chap)
  @chapter = chap
  do_compile
  @strategy.result
end
inline_defined?(name) click to toggle source
# File lib/review/compiler.rb, line 120
def inline_defined?(name)
  INLINE.key?(name.to_sym)
end
syntax_defined?(name) click to toggle source
# File lib/review/compiler.rb, line 102
def syntax_defined?(name)
  SYNTAX.key?(name.to_sym)
end
syntax_descriptor(name) click to toggle source
# File lib/review/compiler.rb, line 106
def syntax_descriptor(name)
  SYNTAX[name.to_sym]
end
text(str) click to toggle source
# File lib/review/compiler.rb, line 516
def text(str)
  return '' if str.empty?
  words = replace_fence(str).split(/(@<\w+>\{(?:[^\}\\]|\\.)*?\})/, -1)
  words.each { |w| error "`@<xxx>' seen but is not valid inline op: #{w}" if w.scan(/@<\w+>/).size > 1 && !/\A@<raw>/.match(w) }
  result = @strategy.nofunc_text(words.shift)
  until words.empty?
    result << compile_inline(words.shift.gsub(/\\\}/, '}').gsub(/\\\\/, '\\'))
    result << @strategy.nofunc_text(words.shift)
  end
  result.gsub("\x01", '@')
rescue => err
  error err.message
end

Private Instance Methods

block_open?(line) click to toggle source
# File lib/review/compiler.rb, line 431
def block_open?(line)
  line.rstrip[-1, 1] == '{'
end
close_all_tagged_section() click to toggle source
# File lib/review/compiler.rb, line 342
def close_all_tagged_section
  close_tagged_section(* @tagged_section.pop) until @tagged_section.empty?
end
close_current_tagged_section(level) click to toggle source
# File lib/review/compiler.rb, line 308
def close_current_tagged_section(level)
  while @tagged_section.last and @tagged_section.last[1] >= level
    close_tagged_section(* @tagged_section.pop)
  end
end
close_tagged_section(tag, level) click to toggle source
# File lib/review/compiler.rb, line 333
def close_tagged_section(tag, level)
  mid = "#{tag}_end"
  if @strategy.respond_to?(mid)
    @strategy.__send__ mid, level
  else
    error "strategy does not support block op: #{mid}"
  end
end
compile_block(syntax, args, lines) click to toggle source
# File lib/review/compiler.rb, line 495
def compile_block(syntax, args, lines)
  @strategy.__send__(syntax.name, (lines || default_block(syntax)), *args)
end
compile_command(syntax, args, lines) click to toggle source
# File lib/review/compiler.rb, line 471
def compile_command(syntax, args, lines)
  unless @strategy.respond_to?(syntax.name)
    error "strategy does not support command: //#{syntax.name}"
    compile_unknown_command args, lines
    return
  end
  begin
    syntax.check_args args
  rescue CompileError => err
    error err.message
    args = ['(NoArgument)'] * syntax.min_argc
  end
  if syntax.block_allowed?
    compile_block syntax, args, lines
  else
    error "block is not allowed for command //#{syntax.name}; ignore" if lines
    compile_single syntax, args
  end
end
compile_dlist(f) click to toggle source
# File lib/review/compiler.rb, line 401
def compile_dlist(f)
  @strategy.dl_begin
  while /\A\s*:/ =~ f.peek
    @strategy.dt text(f.gets.sub(/\A\s*:/, '').strip)
    @strategy.dd(f.break(/\A(\S|\s*:|\s+\d+\.\s|\s+\*\s)/).map { |line| text(line.strip) })
    f.skip_blank_lines
    f.skip_comment_lines
  end
  @strategy.dl_end
end
compile_headline(line) click to toggle source
# File lib/review/compiler.rb, line 279
def compile_headline(line)
  @headline_indexs ||= [@chapter.number.to_i - 1]
  m = /\A(=+)(?:\[(.+?)\])?(?:\{(.+?)\})?(.*)/.match(line)
  level = m[1].size
  tag = m[2]
  label = m[3]
  caption = m[4].strip
  index = level - 1
  if tag
    if tag !~ %r{\A/}
      warn 'headline is empty.' if caption.empty?
      close_current_tagged_section(level)
      open_tagged_section(tag, level, label, caption)
    else
      open_tag = tag[1..-1]
      prev_tag_info = @tagged_section.pop
      error "#{open_tag} is not opened." if prev_tag_info.nil? || prev_tag_info.first != open_tag
      close_tagged_section(*prev_tag_info)
    end
  else
    warn 'headline is empty.' if caption.empty?
    @headline_indexs = @headline_indexs[0..index] if @headline_indexs.size > (index + 1)
    @headline_indexs[index] = 0 if @headline_indexs[index].nil?
    @headline_indexs[index] += 1
    close_current_tagged_section(level)
    @strategy.headline level, label, caption
  end
end
compile_inline(str) click to toggle source
# File lib/review/compiler.rb, line 531
def compile_inline(str)
  op, arg = /\A@<(\w+)>\{(.*?)\}\z/.match(str).captures
  raise CompileError, "no such inline op: #{op}" unless inline_defined?(op)
  raise "strategy does not support inline op: @<#{op}>" unless @strategy.respond_to?("inline_#{op}")
  @strategy.__send__("inline_#{op}", arg)
rescue => err
  error err.message
  @strategy.nofunc_text(str)
end
compile_olist(f) click to toggle source
# File lib/review/compiler.rb, line 388
def compile_olist(f)
  @strategy.ol_begin
  f.while_match(/\A\s+\d+\.|\A\#@/) do |line|
    next if line =~ /\A\#@/

    num = line.match(/(\d+)\./)[1]
    buf = [text(line.sub(/\d+\./, '').strip)]
    f.while_match(/\A\s+(?!\d+\.)\S/) { |cont| buf.push text(cont.strip) }
    @strategy.ol_item buf, num
  end
  @strategy.ol_end
end
compile_paragraph(f) click to toggle source
# File lib/review/compiler.rb, line 412
def compile_paragraph(f)
  buf = []
  f.until_match(%r{\A//|\A\#@}) do |line|
    break if line.strip.empty?
    buf.push text(line.sub(/^(\t+)\s*/) { |m| '<!ESCAPETAB!>' * m.size }.strip.gsub('<!ESCAPETAB!>', "\t"))
  end
  @strategy.paragraph buf
end
compile_single(syntax, args) click to toggle source
# File lib/review/compiler.rb, line 504
def compile_single(syntax, args)
  @strategy.__send__(syntax.name, *args)
end
compile_ulist(f) click to toggle source
# File lib/review/compiler.rb, line 346
def compile_ulist(f)
  level = 0
  f.while_match(/\A\s+\*|\A\#@/) do |line|
    next if line =~ /\A\#@/

    buf = [text(line.sub(/\*+/, '').strip)]
    f.while_match(/\A\s+(?!\*)\S/) { |cont| buf.push text(cont.strip) }

    line =~ /\A\s+(\*+)/
    current_level = $1.size
    if level == current_level
      @strategy.ul_item_end
      # body
      @strategy.ul_item_begin buf
    elsif level < current_level # down
      level_diff = current_level - level
      level = current_level
      (1..(level_diff - 1)).to_a.reverse_each do |i|
        @strategy.ul_begin { i }
        @strategy.ul_item_begin []
      end
      @strategy.ul_begin { level }
      @strategy.ul_item_begin buf
    elsif level > current_level # up
      level_diff = level - current_level
      level = current_level
      (1..level_diff).to_a.reverse_each do |i|
        @strategy.ul_item_end
        @strategy.ul_end { level + i }
      end
      @strategy.ul_item_end
      # body
      @strategy.ul_item_begin buf
    end
  end

  (1..level).to_a.reverse_each do |i|
    @strategy.ul_item_end
    @strategy.ul_end { i }
  end
end
compile_unknown_command(args, lines) click to toggle source
# File lib/review/compiler.rb, line 491
def compile_unknown_command(args, lines)
  @strategy.unknown_command args, lines
end
default_block(syntax) click to toggle source
# File lib/review/compiler.rb, line 499
def default_block(syntax)
  error "block is required for //#{syntax.name}; use empty block" if syntax.block_required?
  []
end
do_compile() click to toggle source
# File lib/review/compiler.rb, line 233
def do_compile
  f = LineInput.new(StringIO.new(@chapter.content))
  @strategy.bind self, @chapter, Location.new(@chapter.basename, f)
  tagged_section_init
  while f.next?
    case f.peek
    when /\A\#@/
      f.gets # Nothing to do
    when /\A=+[\[\s\{]/
      compile_headline f.gets
    when /\A\s+\*/
      compile_ulist f
    when /\A\s+\d+\./
      compile_olist f
    when /\A\s*:\s/
      compile_dlist f
    when %r{\A//\}}
      f.gets
      error 'block end seen but not opened'
    when %r{\A//[a-z]+}
      name, args, lines = read_command(f)
      syntax = syntax_descriptor(name)
      unless syntax
        error "unknown command: //#{name}"
        compile_unknown_command args, lines
        next
      end
      compile_command syntax, args, lines
    when %r{\A//}
      line = f.gets
      warn "`//' seen but is not valid command: #{line.strip.inspect}"
      if block_open?(line)
        warn 'skipping block...'
        read_block(f, false)
      end
    else
      if f.peek.strip.empty?
        f.gets
        next
      end
      compile_paragraph f
    end
  end
  close_all_tagged_section
end
error(msg) click to toggle source
# File lib/review/compiler.rb, line 545
def error(msg)
  @strategy.error msg
end
headline(level, label, caption) click to toggle source
# File lib/review/compiler.rb, line 314
def headline(level, label, caption)
  @strategy.headline level, label, caption
end
open_tagged_section(tag, level, label, caption) click to toggle source
# File lib/review/compiler.rb, line 322
def open_tagged_section(tag, level, label, caption)
  mid = "#{tag}_begin"
  unless @strategy.respond_to?(mid)
    error "strategy does not support tagged section: #{tag}"
    headline level, label, caption
    return
  end
  @tagged_section.push [tag, level]
  @strategy.__send__ mid, level, label, caption
end
parse_args(str, _name = nil) click to toggle source
# File lib/review/compiler.rb, line 453
def parse_args(str, _name = nil)
  return [] if str.empty?
  scanner = StringScanner.new(str)
  words = []
  while word = scanner.scan(/(\[\]|\[.*?[^\\]\])/)
    w2 = word[1..-2].gsub(/\\(.)/) do
      ch = $1
      (ch == ']' or ch == '\\') ? ch : '\\' + ch
    end
    words << w2
  end
  unless scanner.eos?
    error "argument syntax error: #{scanner.rest} in #{str.inspect}"
    return []
  end
  words
end
read_block(f, ignore_inline) click to toggle source
# File lib/review/compiler.rb, line 435
def read_block(f, ignore_inline)
  head = f.lineno
  buf = []
  f.until_match(%r{\A//\}}) do |line|
    if ignore_inline
      buf.push line
    elsif line !~ /\A\#@/
      buf.push text(line.rstrip)
    end
  end
  unless %r{\A//\}} =~ f.peek
    error "unexpected EOF (block begins at: #{head})"
    return buf
  end
  f.gets # discard terminator
  buf
end
read_command(f) click to toggle source
# File lib/review/compiler.rb, line 421
def read_command(f)
  line = f.gets
  name = line.slice(/[a-z]+/).to_sym
  ignore_inline = (name == :embed)
  args = parse_args(line.sub(%r{\A//[a-z]+}, '').rstrip.chomp('{'), name)
  lines = block_open?(line) ? read_block(f, ignore_inline) : nil

  [name, args, lines]
end
replace_fence(str) click to toggle source
# File lib/review/compiler.rb, line 508
def replace_fence(str)
  str.gsub(/@<(\w+)>([$|])(.+?)(\2)/) do
    op = $1
    arg = $3.gsub('@', "\x01").gsub('\\}') { '\\\\}' }.gsub('}') { '\}' }.sub(/(?:\\)+$/) { |m| '\\\\' * m.size }
    "@<#{op}>{#{arg}}"
  end
end
tagged_section_init() click to toggle source
# File lib/review/compiler.rb, line 318
def tagged_section_init
  @tagged_section = []
end
warn(msg) click to toggle source
# File lib/review/compiler.rb, line 541
def warn(msg)
  @strategy.warn msg
end