# -*- coding: utf-8 -*- # rubocop:disable all # Rubyのソースコードをブロックに変換するモジュール module RubyToBlock extend ActiveSupport::Concern SUCCESS_DATA_MOCK = <<-EOS.strip_heredoc require "smalruby" car1 = Character.new(costume: "car1.png", x: 0, y: 0, angle: 0) car1.on(:start) do loop do move(10) turn_if_reach_wall end end EOS SUCCESS_XML_MOCK = <<-XML.strip_heredoc car1 10 XML # XML形式のブロックに変換する def to_blocks fail if data == '__FAIL__' return SUCCESS_XML_MOCK if data == SUCCESS_DATA_MOCK characters = {} character_stack = [] receiver_stack = [] statement_stack = [] blocks = [] current_block = nil lines = data.lines while (line = lines.shift) line.chomp! next if line.strip.empty? md = STATEMENT_REGEXP.match(line) unless md block = Block.new('ruby_statement', fields: { STATEMENT: line }) if current_block # TODO: リファクタリング current_block.sibling = block else blocks.push(block) end current_block = block next end next if md[:require_smalruby] if (s = md[:character]) md2 = /#{CHARACTER_RE}/.match(s) name = md2[1] characters[name] = Character.new(name: name, costumes: [md2[2]], x: md2[3], y: md2[4], angle: md2[5]) next end if (s = md[:events_on_start]) md2 = /#{EVENTS_ON_START_RE}/.match(s) name = md2[1] ? md2[1] : receiver_stack.last.name c = characters[name] character_stack.push(c) do_block = Block.new('null') events_on_start_block = Block.new('events_on_start', statements: { DO: do_block }) if current_block && current_block.type == 'character_new' && current_block[:NAME] == name current_block.add_statement(:DO, events_on_start_block) character_new_block = current_block else if c == receiver_stack.last current_block.sibling = events_on_start_block character_new_block = current_block.parent else character_new_block = Block.new('character_new', fields: { NAME: name }, statements: { DO: events_on_start_block }) if current_block if current_block.type == 'character_new' blocks.push(character_new_block) else current_block.sibling = character_new_block end else blocks.push(character_new_block) end end end statement_stack.push([:events_on_start, character_new_block]) receiver_stack.push(c) current_block = do_block next end if md[:end] ends_num = lines.select { |l| md2 = STATEMENT_REGEXP.match(l) md2 && md2[:end] }.length + 1 ends_num -= lines.select { |l| md2 = STATEMENT_REGEXP.match(l) md2 && (md2[:events_on_start]) }.length if (ss = statement_stack.last) && ends_num <= statement_stack.length case ss.first when :events_on_start current_block = ss[1] receiver_stack.pop character_stack.pop statement_stack.pop else # TODO end else block = Block.new('ruby_statement', fields: { STATEMENT: line }) if current_block # TODO: リファクタリング current_block.sibling = block else blocks.push(block) end current_block = block end next end end return '' if characters.empty? && blocks.empty? xml = REXML::Document.new('', attribute_quote: :quote, respect_whitespace: :all) characters.values.each do |c| c.to_xml(xml.root) end blocks.each do |b| b.to_xml(xml.root) end output = StringIO.new formatter = REXML::Formatters::Pretty.new(2, true) formatter.compact = true # HACK: 行頭、行末などのスペースが1つになってしまうのを修正している def formatter.write_text( node, output ) s = node.to_s() s.gsub!(/\s/,' ') if !node.is_a?(REXML::Text) || node.is_a?(REXML::Text) && !node.parent.whitespace() s.squeeze!(" ") end s = wrap(s, @width - @level) s = indent_text(s, @level, " ", true) output << (' '*@level + s) end formatter.write(xml, output) output.string + "\n" end private # require "smalruby" REQUIRE_SMALRUBY_RE = '^require\ "smalruby"$' # car1 = Character.new(costume: "car1.png", x: 0, y: 0, angle: 0) CHARACTER_RE = '^\s*(\S+)\ =\ Character\.new\(costume:\ "([^"]+)",\ x:\ (\d+),\ y:\ (\d+),\ angle:\ (\d+)\)$' EVENTS_ON_START_RE = '^\s*(?:(\S+)\.)?on\(:start\)\ do$' END_RE = '^\s*end$' STATEMENT_REGEXP = %r{ (?#{REQUIRE_SMALRUBY_RE}) | (?#{CHARACTER_RE}) | (?#{EVENTS_ON_START_RE}) | (?#{END_RE}) }x EXPRESSION_REGEXP = %r{}x LITERAL_REGEXP = %r{}x # ソースコードに含まれるキャラクターを表現するクラス class Character attr_accessor :name attr_accessor :costumes attr_accessor :x attr_accessor :y attr_accessor :angle def initialize(options) @name = options[:name] @costumes = options[:costumes] @x = options[:x] @y = options[:y] @angle = options[:angle] end def to_xml(parent) parent.add_element('character', 'name' => @name, 'x' => @x, 'y' => @y, 'angle' => @angle, 'costumes' => @costumes.join(',')) end end # ブロック群を表現するモジュール module Block # ブロックのインスタンスを生成する def self.new(type, *args) const_get(type.camelize).new(*args) end # すべてのブロックのベースクラス class Base attr_accessor :parent attr_accessor :sibling attr_accessor :fields attr_accessor :values attr_accessor :statements def initialize(options = {}) @fields = options[:fields] || {} @values = options[:values] || {} @statements = options[:statements] || {} if @statements.length > 0 @statements.values.each do |s| s.parent = self end end end def to_xml(parent) e = parent.add_element('block', 'type' => type) e.add_attribute('inline', 'true') if inline? fields_to_xml(e) values_to_xml(e) statements_to_xml(e) sibling_to_xml(e) e end def type @type ||= self.class.name.sub('RubyToBlock::Block::', '').underscore end def inline? false end def null? false end def [](name) @fields[name] end def add_statement(name, block) b = @statements[name] b = b.sibling while b.sibling b.sibling = block end def sibling=(block) block.parent = self.parent @sibling = block end def indent? false end def indent_level b = self level = 0 while b.parent level += 1 if b.indent? b = b.parent end level end private def fields_to_xml(parent) @fields.each do |k, v| e = parent.add_element('field', 'name' => k.to_s) if v.is_a?(String) e.text = v else # TODO end end end def values_to_xml(parent) @values.each do |k, v| # TODO end end def statements_to_xml(parent) @statements.each do |k, v| next if v.null? e = parent.add_element('statement', 'name' => k.to_s) v.to_xml(e) end end def sibling_to_xml(parent) return nil unless @sibling e = parent.add_element('next') @sibling.to_xml(e) end end class Null < Base def to_xml(parent) return nil unless @sibling @sibling.to_xml(parent) end def null? !@sibling end end class CharacterNew < Base; end class RubyStatement < Base def parent=(block) @parent = block @original_statement ||= @fields[:STATEMENT] @fields[:STATEMENT] = @original_statement.sub(/^ {0,#{indent_level * 2}}/, '') end end class EventsOnStart < Base def indent? true end end end end