class ReVIEW::Compiler

Constants

INLINE
MAX_HEADLINE_LEVEL
SYNTAX

Attributes

builder[R]
previous_list_type[R]

Public Class Methods

defblock(name, argc, optional = false, &block) click to toggle source
# File lib/review/compiler.rb, line 107
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 123
def self.definline(name)
  INLINE[name] = InlineSyntaxElement.new(name)
end
defminicolumn(name, argc, _optional = false, &block) click to toggle source
# File lib/review/compiler.rb, line 111
def self.defminicolumn(name, argc, _optional = false, &block)
  defsyntax(name, :minicolumn, argc, &block)
end
defsingle(name, argc, &block) click to toggle source
# File lib/review/compiler.rb, line 115
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 119
def self.defsyntax(name, type, argc, &block)
  SYNTAX[name] = SyntaxElement.new(name, type, argc, &block)
end
minicolumn_names() click to toggle source
# File lib/review/compiler.rb, line 127
def self.minicolumn_names
  buf = []
  SYNTAX.each do |name, syntax|
    if syntax.minicolumn?
      buf << name.to_s
    end
  end
  buf
end
new(builder) click to toggle source
# File lib/review/compiler.rb, line 22
def initialize(builder)
  @builder = builder

  ## commands which do not parse block lines in compiler
  @non_parsed_commands = %i[embed texequation graph]

  ## to decide escaping/non-escaping for text
  @command_name_stack = []

  @logger = ReVIEW.logger

  @ignore_errors = builder.is_a?(ReVIEW::IndexBuilder)

  @compile_errors = nil
end

Public Instance Methods

compile(chap) click to toggle source
# File lib/review/compiler.rb, line 53
def compile(chap)
  @chapter = chap
  do_compile
  if @compile_errors
    raise ApplicationError, "#{location.filename} cannot be compiled."
  end

  @builder.result
end
inline_defined?(name) click to toggle source
# File lib/review/compiler.rb, line 155
def inline_defined?(name)
  INLINE.key?(name.to_sym)
end
non_escaped_commands() click to toggle source
# File lib/review/compiler.rb, line 45
def non_escaped_commands
  if @builder.highlight?
    %i[list emlist listnum emlistnum cmd source]
  else
    []
  end
end
strategy() click to toggle source
# File lib/review/compiler.rb, line 40
def strategy
  error 'Compiler#strategy is obsoleted. Use Compiler#builder.'
  @builder
end
syntax_defined?(name) click to toggle source
# File lib/review/compiler.rb, line 137
def syntax_defined?(name)
  SYNTAX.key?(name.to_sym)
end
syntax_descriptor(name) click to toggle source
# File lib/review/compiler.rb, line 141
def syntax_descriptor(name)
  SYNTAX[name.to_sym]
end
text(str, block_mode = false) click to toggle source
# File lib/review/compiler.rb, line 666
def text(str, block_mode = false)
  return '' if str.empty?

  words = replace_fence(str).split(/(@<\w+>\{(?:[^}\\]|\\.)*?\})/, -1)
  words.each do |w|
    if w.scan(/@<\w+>/).size > 1 && !/\A@<raw>/.match(w)
      error "`@<xxx>' seen but is not valid inline op: #{w}", location: location
    end
  end
  result = ''
  until words.empty?
    result << if in_non_escaped_command? && block_mode
                revert_replace_fence(words.shift)
              else
                @builder.nofunc_text(revert_replace_fence(words.shift))
              end
    break if words.empty?

    result << compile_inline(revert_replace_fence(words.shift.gsub('\\}', '}').gsub('\\\\', '\\')))
  end
  result
rescue StandardError => e
  error e.message, location: location
end

Private Instance Methods

block_open?(line) click to toggle source
# File lib/review/compiler.rb, line 568
def block_open?(line)
  line.rstrip[-1, 1] == '{'
end
close_all_tagged_section() click to toggle source
# File lib/review/compiler.rb, line 464
def close_all_tagged_section
  until @tagged_section.empty?
    close_tagged_section(* @tagged_section.pop)
  end
end
close_current_tagged_section(level) click to toggle source
# File lib/review/compiler.rb, line 430
def close_current_tagged_section(level)
  while @tagged_section.last && (@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 455
def close_tagged_section(tag, level)
  mid = "#{tag}_end"
  if @builder.respond_to?(mid)
    @builder.__send__(mid, level)
  else
    error "builder does not support block op: #{mid}", location: location
  end
end
compile_block(syntax, args, lines) click to toggle source
# File lib/review/compiler.rb, line 630
def compile_block(syntax, args, lines)
  @builder.__send__(syntax.name, (lines || default_block(syntax)), *args)
end
compile_command(syntax, args, lines) click to toggle source
# File lib/review/compiler.rb, line 609
def compile_command(syntax, args, lines)
  unless @builder.respond_to?(syntax.name)
    error "builder does not support command: //#{syntax.name}", location: location
    return
  end
  begin
    syntax.check_args(args)
  rescue CompileError => e
    error e.message, location: location
    args = ['(NoArgument)'] * syntax.min_argc
  end
  if syntax.block_allowed?
    compile_block(syntax, args, lines)
  else
    if lines
      error "block is not allowed for command //#{syntax.name}; ignore", location: location
    end
    compile_single(syntax, args)
  end
end
compile_dlist(f) click to toggle source
# File lib/review/compiler.rb, line 528
def compile_dlist(f)
  @builder.dl_begin
  while /\A\s*:/ =~ f.peek
    # defer compile_inline to handle footnotes
    @builder.doc_status[:dt] = true
    @builder.dt(text(f.gets.sub(/\A\s*:/, '').strip))
    @builder.doc_status[:dt] = nil
    desc = []
    f.until_match(/\A(\S|\s*:|\s+\d+\.\s|\s+\*\s)/) do |line|
      desc << text(line.strip)
    end
    @builder.dd(desc)
    f.skip_blank_lines
    f.skip_comment_lines
  end
  @builder.dl_end
end
compile_headline(line) click to toggle source
# File lib/review/compiler.rb, line 387
def compile_headline(line)
  @headline_indexs ||= [@chapter.number.to_i - 1]
  m = /\A(=+)(?:\[(.+?)\])?(?:\{(.+?)\})?(.*)/.match(line)
  level = m[1].size
  if level > MAX_HEADLINE_LEVEL
    raise CompileError, "Invalid header: max headline level is #{MAX_HEADLINE_LEVEL}"
  end

  tag = m[2]
  label = m[3]
  caption = m[4].strip
  index = level - 1
  if tag
    if tag.start_with?('/')
      open_tag = tag[1..-1]
      prev_tag_info = @tagged_section.pop
      if prev_tag_info.nil? || prev_tag_info.first != open_tag
        error "#{open_tag} is not opened.", location: location
      end
      close_tagged_section(*prev_tag_info)
    else
      if caption.empty?
        warn 'headline is empty.', location: location
      end
      close_current_tagged_section(level)
      open_tagged_section(tag, level, label, caption)
    end
  else
    if caption.empty?
      warn 'headline is empty.', location: location
    end
    if @headline_indexs.size > (index + 1)
      @headline_indexs = @headline_indexs[0..index]
    end
    if @headline_indexs[index].nil?
      @headline_indexs[index] = 0
    end
    @headline_indexs[index] += 1
    close_current_tagged_section(level)
    @builder.headline(level, label, caption)
  end
end
compile_inline(str) click to toggle source
# File lib/review/compiler.rb, line 692
def compile_inline(str)
  op, arg = /\A@<(\w+)>\{(.*?)\}\z/.match(str).captures
  unless inline_defined?(op)
    raise CompileError, "no such inline op: #{op}"
  end
  unless @builder.respond_to?("inline_#{op}")
    raise "builder does not support inline op: @<#{op}>"
  end

  @builder.__send__("inline_#{op}", arg)
rescue StandardError => e
  error e.message, location: location
  @builder.nofunc_text(str)
end
compile_minicolumn_begin(name, caption = nil) click to toggle source
# File lib/review/compiler.rb, line 360
def compile_minicolumn_begin(name, caption = nil)
  mid = "#{name}_begin"
  unless @builder.respond_to?(mid)
    error "strategy does not support minicolumn: #{name}", location: location
  end

  if @minicolumn_name
    error "minicolumn cannot be nested: #{name}", location: location
    return
  end
  @minicolumn_name = name

  @builder.__send__(mid, caption)
end
compile_minicolumn_end() click to toggle source
# File lib/review/compiler.rb, line 375
def compile_minicolumn_end
  unless @minicolumn_name
    error "minicolumn is not used: #{name}", location: location
    return
  end
  name = @minicolumn_name

  mid = "#{name}_end"
  @builder.__send__(mid)
  @minicolumn_name = nil
end
compile_olist(f) click to toggle source
# File lib/review/compiler.rb, line 513
def compile_olist(f)
  @builder.ol_begin
  f.while_match(/\A\s+\d+\.|\A\#@/) do |line|
    next if /\A\#@/.match?(line)

    num = line.match(/(\d+)\./)[1]
    buf = [text(line.sub(/\d+\./, '').strip)]
    f.while_match(/\A\s+(?!\d+\.)\S/) do |cont|
      buf.push(text(cont.strip))
    end
    @builder.ol_item(buf, num)
  end
  @builder.ol_end
end
compile_paragraph(f) click to toggle source
# File lib/review/compiler.rb, line 546
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
  @builder.paragraph(buf)
end
compile_single(syntax, args) click to toggle source
# File lib/review/compiler.rb, line 641
def compile_single(syntax, args)
  @builder.__send__(syntax.name, *args)
end
compile_ulist(f) click to toggle source
# File lib/review/compiler.rb, line 470
def compile_ulist(f)
  level = 0
  f.while_match(/\A\s+\*|\A\#@/) do |line|
    next if /\A\#@/.match?(line)

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

    line =~ /\A\s+(\*+)/
    current_level = $1.size
    if level == current_level
      @builder.ul_item_end
      # body
      @builder.ul_item_begin(buf)
    elsif level < current_level # down
      level_diff = current_level - level
      if level_diff != 1
        error 'too many *.', location: location
      end
      level = current_level
      @builder.ul_begin { level }
      @builder.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|
        @builder.ul_item_end
        @builder.ul_end { level + i }
      end
      @builder.ul_item_end
      # body
      @builder.ul_item_begin(buf)
    end
  end

  (1..level).to_a.reverse_each do |i|
    @builder.ul_item_end
    @builder.ul_end { i }
  end
end
default_block(syntax) click to toggle source
# File lib/review/compiler.rb, line 634
def default_block(syntax)
  if syntax.block_required?
    error "block is required for //#{syntax.name}; use empty block", location: location
  end
  []
end
do_compile() click to toggle source
# File lib/review/compiler.rb, line 280
def do_compile
  f = LineInput.new(StringIO.new(@chapter.content))
  @builder.bind(self, @chapter, Location.new(@chapter.basename, f))
  @previous_list_type = nil

  ## in minicolumn, such as note/info/alert...
  @minicolumn_name = nil

  tagged_section_init
  while f.next?
    case f.peek
    when /\A\#@/
      f.gets # Nothing to do
    when /\A=+[\[\s{]/
      compile_headline(f.gets)
      @previous_list_type = nil
    when /\A\s+\*/
      compile_ulist(f)
      @previous_list_type = 'ul'
    when /\A\s+\d+\./
      compile_olist(f)
      @previous_list_type = 'ol'
    when /\A\s+:\s/
      compile_dlist(f)
      @previous_list_type = 'dl'
    when /\A\s*:\s/
      warn 'Definition list starting with `:` is deprecated. It should start with ` : `.', location: location
      compile_dlist(f)
      @previous_list_type = 'dl'
    when %r{\A//\}}
      if in_minicolumn?
        _line = f.gets
        compile_minicolumn_end
      else
        f.gets
        error 'block end seen but not opened', location: location
      end
    when %r{\A//[a-z]+}
      line = f.peek
      matched = line =~ %r|\A//([a-z]+)(:?\[.*\])?{\s*$|
      if matched && minicolumn_block_name?($1)
        line = f.gets
        name = $1
        args = parse_args(line.sub(%r{\A//[a-z]+}, '').rstrip.chomp('{'), name)
        compile_minicolumn_begin(name, *args)
      else
        # @command_name_stack.push(name) ## <- move into read_command() to use name
        name, args, lines = read_command(f)
        syntax = syntax_descriptor(name)
        unless syntax
          error "unknown command: //#{name}", location: location
          @command_name_stack.pop
          next
        end
        compile_command(syntax, args, lines)
        @command_name_stack.pop
      end
      @previous_list_type = nil
    when %r{\A//}
      line = f.gets
      warn "`//' seen but is not valid command: #{line.strip.inspect}", location: location
      if block_open?(line)
        warn 'skipping block...', location: location
        read_block(f, false)
      end
      @previous_list_type = nil
    else
      if f.peek.strip.empty?
        f.gets
        next
      end
      compile_paragraph(f)
      @previous_list_type = nil
    end
  end
  close_all_tagged_section
rescue SyntaxError => e
  error e, location: location
end
error(msg, location: nil) click to toggle source

override

Calls superclass method ReVIEW::Loggable#error
# File lib/review/compiler.rb, line 724
def error(msg, location: nil)
  return if ignore_errors? # for IndexBuilder

  @compile_errors = true
  super
end
headline(level, label, caption) click to toggle source
# File lib/review/compiler.rb, line 436
def headline(level, label, caption)
  @builder.headline(level, label, caption)
end
ignore_errors?() click to toggle source
# File lib/review/compiler.rb, line 715
def ignore_errors?
  @ignore_errors
end
in_minicolumn?() click to toggle source
# File lib/review/compiler.rb, line 707
def in_minicolumn?
  @builder.in_minicolumn?
end
in_non_escaped_command?() click to toggle source
# File lib/review/compiler.rb, line 661
def in_non_escaped_command?
  current_command = @command_name_stack.last
  current_command && non_escaped_commands.include?(current_command)
end
location() click to toggle source
# File lib/review/compiler.rb, line 719
def location
  @builder.location
end
minicolumn_block_name?(name) click to toggle source
# File lib/review/compiler.rb, line 711
def minicolumn_block_name?(name)
  @builder.minicolumn_block_name?(name)
end
open_tagged_section(tag, level, label, caption) click to toggle source
# File lib/review/compiler.rb, line 444
def open_tagged_section(tag, level, label, caption)
  mid = "#{tag}_begin"
  unless @builder.respond_to?(mid)
    error "builder does not support tagged section: #{tag}", location: location
    headline(level, label, caption)
    return
  end
  @tagged_section.push([tag, level])
  @builder.__send__(mid, level, label, caption)
end
parse_args(str, _name = nil) click to toggle source
# File lib/review/compiler.rb, line 590
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
      [']', '\\'].include?(ch) ? ch : '\\' + ch
    end
    words << w2
  end
  unless scanner.eos?
    error "argument syntax error: #{scanner.rest} in #{str.inspect}", location: location
    return []
  end
  words
end
read_block(f, ignore_inline) click to toggle source
# File lib/review/compiler.rb, line 572
def read_block(f, ignore_inline)
  head = f.lineno
  buf = []
  f.until_match(%r{\A//\}}) do |line|
    if ignore_inline
      buf.push(line.chomp)
    elsif !/\A\#@/.match?(line)
      buf.push(text(line.rstrip, true))
    end
  end
  unless f.peek.to_s.start_with?('//}')
    error "unexpected EOF (block begins at: #{head})", location: location
    return buf
  end
  f.gets # discard terminator
  buf
end
read_command(f) click to toggle source
# File lib/review/compiler.rb, line 556
def read_command(f)
  line = f.gets
  name = line.slice(/[a-z]+/).to_sym
  ignore_inline = @non_parsed_commands.include?(name)
  @command_name_stack.push(name)
  args = parse_args(line.sub(%r{\A//[a-z]+}, '').rstrip.chomp('{'), name)
  @builder.doc_status[name] = true
  lines = block_open?(line) ? read_block(f, ignore_inline) : nil
  @builder.doc_status[name] = nil
  [name, args, lines]
end
replace_fence(str) click to toggle source
# File lib/review/compiler.rb, line 645
def replace_fence(str)
  str.gsub(/@<(\w+)>([$|])(.+?)(\2)/) do
    op = $1
    arg = $3
    if /[\x01\x02\x03\x04]/.match?(arg)
      error "invalid character in '#{str}'", location: location
    end
    replaced = arg.tr('@', "\x01").tr('\\', "\x02").tr('{', "\x03").tr('}', "\x04")
    "@<#{op}>{#{replaced}}"
  end
end
revert_replace_fence(str) click to toggle source
# File lib/review/compiler.rb, line 657
def revert_replace_fence(str)
  str.tr("\x01", '@').tr("\x02", '\\').tr("\x03", '{').tr("\x04", '}')
end
tagged_section_init() click to toggle source
# File lib/review/compiler.rb, line 440
def tagged_section_init
  @tagged_section = []
end