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