Created
February 22, 2014 14:25
-
-
Save zarkzork/9155609 to your computer and use it in GitHub Desktop.
single pass converter semi-automatic converter from erb to dust
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'strscan' | |
# single pass converter semi-automatic converter from erb to dust | |
input_stream = $stdin.read | |
# Class to parse ruby line and detect its type, filling symbol table | |
# with references. | |
class RubyLine | |
def initialize(line, symbol_table) | |
@line = line | |
@scanner = StringScanner.new(line) | |
@symbol_table = symbol_table | |
end | |
# Parses ruby line and retruns its type in terms of logic flow | |
def parse | |
@scanner.skip(/\s+/) | |
token, line = nil | |
type = [:else, :end, :block, :elsif, :if, :statement].find do |o| | |
token, line = self.send("parse_#{o}") | |
end | |
if line | |
@symbol_table[token] ||= [type, line] | |
end | |
[type, token] | |
end | |
private | |
def generate_token | |
@@index ||= 0 | |
"token:#{@@index += 1}" | |
end | |
# WARINING: | |
# all parse_* methods MUST not move @scanner.pos unless they succeded | |
def parse_else | |
@scanner.scan(/\belse\s*/) | |
end | |
def parse_end | |
@scanner.scan(/\bend\s*/) | |
end | |
def parse_block | |
return unless @scanner.exist?(/\bdo\s+(?:|[^|]*|)?/) | |
parse_statement | |
end | |
def parse_elsif | |
return unless @scanner.scan(/\belsif\s+/) | |
parse_statement | |
end | |
def parse_if | |
return unless @scanner.scan(/\bif\s+/) | |
parse_statement | |
end | |
def parse_statement | |
return [generate_token, @scanner.rest] | |
end | |
end | |
# Here we start parsing our file for ERB tags | |
out_buffer = '' | |
stack = [] | |
symbol_table = {} | |
scanner = StringScanner.new(input_stream) | |
while !scanner.eos? | |
ruby_line = '' | |
out_buffer << scanner.getch until scanner.check(/<%-?/) || scanner.eos? | |
break if scanner.eos? | |
scanner.skip(/<%-?/) | |
next if scanner.scan(/#/) | |
need_output = !!scanner.scan(/=/) | |
scanner.skip(/\s*/) | |
ruby_line << scanner.getch until scanner.check(/=?-?%>/) | |
scanner.skip(/=?-?%>/) | |
type, dust_command = RubyLine.new(ruby_line.gsub("\n", '; '), symbol_table).parse | |
case type | |
when :elsif | |
stack.push [:elsif, dust_command] | |
out_buffer << "{:else}" | |
out_buffer << "{?#{dust_command}}" | |
when :else | |
out_buffer << "{:else}" | |
when :if | |
stack.push [:if, dust_command] | |
out_buffer << "{?#{dust_command}}" | |
when :block | |
stack.push [:block, dust_command] | |
out_buffer << "{##{dust_command}}" | |
when :statement | |
out_buffer << "{#{dust_command}}" if need_output | |
when :end | |
begin | |
stack_type, stack_value = stack.pop | |
out_buffer << "{/#{stack_value}}" | |
end until stack_type != :elsif | |
end | |
end | |
raise "STACK IS NOT EMPTY" unless stack.empty? | |
class Simplifier | |
def initialize(expression) | |
@expression = expression | |
end | |
def simplify | |
if new_expression = recursive_simplify(@expression) | |
[:replace, new_expression] | |
else | |
[:keep, @expression] | |
end | |
end | |
private | |
# expression consist of strings, or touples [type, line]. | |
def recursive_simplify(expression) | |
new_expression = expression.inject([]) do |acc, element| | |
next acc << element if element.is_a?(String) | |
type, line = element | |
_, _, rule = self.class.rules.find do |rule_type, rule_regexp, _| | |
rule_type == type && line =~ rule_regexp | |
end | |
next acc << element unless rule | |
acc + rule.call(line) | |
end | |
case | |
when new_expression != expression; recursive_simplify(new_expression) | |
when new_expression.all?{ |o| o.is_a?(String) }; new_expression.join | |
else | |
nil | |
end | |
end | |
class << self | |
def add_rule(type, regexp, &block) | |
self.rules.unshift([type, regexp, block]) | |
end | |
def rules | |
@@rules ||= [] | |
end | |
end | |
end | |
TOKEN = /[a-z_][A-Za-z0-9_]*\??/ | |
VAR = /@?(?<varname>#{TOKEN})/ | |
ACCESS_CHAIN = /(?<access_chain>\.#{TOKEN}\??)+/ | |
BLOCK = /\bdo\s+(?:|[^|]*|)?/ | |
SIMPLE_STATEMENT = /^#{VAR}#{ACCESS_CHAIN}*$/ | |
# h2. Blocks | |
Simplifier.add_rule :block, // do |line| | |
["#", [:block_statement, line]] | |
end | |
Simplifier.add_rule :block_statement, SIMPLE_STATEMENT do |line| | |
r = Regexp.new(SIMPLE_STATEMENT) | |
match_data = r.match(line) | |
dust_line = "#{match_data[:varname]}#{match_data[:access_chain]}" | |
[dust_line.sub(/[?!]/, '')] | |
end | |
# h2. Conditinals | |
Simplifier.add_rule :if_statement, SIMPLE_STATEMENT do |line| | |
r = Regexp.new(SIMPLE_STATEMENT) | |
match_data = r.match(line) | |
dust_line = "#{match_data[:varname]}#{match_data[:access_chain]}" | |
[dust_line.sub(/[?!]/, '')] | |
end | |
# add conditinals prefixes | |
Simplifier.add_rule :if, // do |line| | |
["{?", [:if_statement, line], "}"] | |
end | |
Simplifier.add_rule :if, /^!/ do |line| | |
["{^", [:if_statement, line[1..-1]], "}"] | |
end | |
# Split two conditions | |
Simplifier.add_rule :if, /&&/ do |line| | |
first, second = line.split(/&&/) | |
[[:if, first], "}{", [:if, second]] | |
end | |
# turn elsif into if | |
Simplifier.add_rule :elsif, // do |line| | |
[[:if, line]] | |
end | |
# h2. Statements | |
Simplifier.add_rule :statement, SIMPLE_STATEMENT do |line| | |
r = Regexp.new(SIMPLE_STATEMENT) | |
match_data = r.match(line) | |
dust_line = "#{match_data[:varname]}#{match_data[:access_chain]}" | |
[dust_line.sub(/[?!]/, '')] | |
end | |
Simplifier.add_rule :statement, /^h\((.*)\)$/ do |line| | |
[[:statement, line.gsub(/h\((.*)\)/, '\1')]] | |
end | |
Simplifier.add_rule :statement, /^raw (.*)$/ do |line| | |
[[:statement, line.gsub(/^raw\(?(.*)\)?$/, '\1')], '|s'] | |
end | |
Simplifier.add_rule :statement, /^date\((.*)\)$/ do |line| | |
[[:statement, line.gsub(/^date\((.*)\)$/, '\1')], '|d'] | |
end | |
Simplifier.add_rule :statement, /^time\((.*)\)$/ do |line| | |
[[:statement, line.gsub(/^time\((.*)\)$/, '\1')], '|t'] | |
end | |
Simplifier.add_rule :statement, /^user_path\((.*)\)$/ do |line| | |
['/users/', [:statement, line.gsub(/^user_path\((.*)\)$/, '\1')]] | |
end | |
# h2. Custom conditionals | |
# h2. Clean whitespace | |
Simplifier.add_rule :statement, /^\s+|\s+$/ do |line| | |
line = line.dup | |
line.gsub!(/^\s*/, '') | |
line.gsub!(/\s*$/, '') | |
[[:statement, line]] | |
end | |
Simplifier.add_rule :block, /^\s+|\s+$/ do |line| | |
line = line.dup | |
line.gsub!(/^\s*/, '') | |
line.gsub!(/\s*$/, '') | |
[[:block, line]] | |
end | |
Simplifier.add_rule :if, /^\s+|\s+$/ do |line| | |
line = line.dup | |
line.gsub!(/^\s*/, '') | |
line.gsub!(/\s*$/, '') | |
[[:if, line]] | |
end | |
# Simplify expressions | |
operations = symbol_table | |
.sort { |(k1, v1),(k2, v2)| k1.sub('token:', '').to_i <=> k2.sub('token:', '').to_i } | |
.map { |k, (type, line)| [k, type, Simplifier.new([[type, line]]).simplify] } | |
close_statement = lambda do |statement| | |
statement.gsub(/{[?^#]/, '{/') | |
end | |
# apply simplify rules on buffer | |
operations.each do |token, type, (operation, line)| | |
next unless operation == :replace | |
case type | |
when :elsif, :if, :block | |
out_buffer.gsub!(/{[?#^]#{token}}/, line) | |
out_buffer.gsub!(/{\/#{token}}/, close_statement.call(line)) | |
else | |
out_buffer.gsub!(/{#{token}}/, line) | |
end | |
end | |
# print result | |
operations.each do |token, type, (operation, line)| | |
next unless operation == :keep | |
puts [token, line].join(' ') | |
end | |
puts "---" | |
print out_buffer |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
worse is better ftw.