Skip to content

Instantly share code, notes, and snippets.

@zarkzork
Created February 22, 2014 14:25
Show Gist options
  • Save zarkzork/9155609 to your computer and use it in GitHub Desktop.
Save zarkzork/9155609 to your computer and use it in GitHub Desktop.
single pass converter semi-automatic converter from erb to dust
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
@zarkzork
Copy link
Author

worse is better ftw.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment