├── spec ├── spec_helper.rb └── sith_spec.rb ├── examples ├── example1.rb ├── simple.rb ├── example0.rb └── attributes.rb ├── .travis.yml ├── lib ├── sith.rb └── sith │ ├── macro_expander.rb │ ├── loader.rb │ └── macro.rb ├── Gemfile ├── bin └── sith ├── Rakefile ├── sith.gemspec ├── LICENSE └── README.md /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example1.rb: -------------------------------------------------------------------------------- 1 | def a 2 | simple 2, 4 3 | end 4 | -------------------------------------------------------------------------------- /examples/simple.rb: -------------------------------------------------------------------------------- 1 | macro simple(a, b) 2 | ~{a} + ~{b} 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.1 4 | script: 5 | - bundle exec rake 6 | -------------------------------------------------------------------------------- /examples/example0.rb: -------------------------------------------------------------------------------- 1 | class A 2 | attr_reader a 3 | attr_writer b 4 | attr_accessor c, d 5 | end 6 | -------------------------------------------------------------------------------- /lib/sith.rb: -------------------------------------------------------------------------------- 1 | require_relative 'sith/macro' 2 | require_relative 'sith/loader' 3 | require_relative 'sith/macro_expander' 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'parser' 4 | gem 'unparser' 5 | 6 | group :development, :test do 7 | gem 'rake' 8 | gem 'jeweler' 9 | gem 'rspec' 10 | gem 'bundler' 11 | end 12 | -------------------------------------------------------------------------------- /examples/attributes.rb: -------------------------------------------------------------------------------- 1 | macro_mapper attr_reader(label) 2 | def ~{label} 3 | @~{label} 4 | end 5 | end 6 | 7 | macro_mapper attr_writer(label) 8 | def ~{label}=(value) 9 | @~{label} = value 10 | end 11 | end 12 | 13 | macro attr_accessor(*labels) 14 | attr_reader ~{labels} 15 | attr_writer ~{labels} 16 | end 17 | 18 | -------------------------------------------------------------------------------- /bin/sith: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 4 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 5 | require 'sith' 6 | 7 | ruby_file, filename = ARGV.first, ARGV[1] 8 | 9 | ruby_source = File.read(ruby_file) 10 | macro_source = File.read(filename) 11 | 12 | expander = Sith::MacroExpander.new(Sith::load_macros(macro_source)) 13 | puts expander.expand_to_source(ruby_source) 14 | 15 | 16 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | begin 6 | Bundler.setup(:default, :development) 7 | rescue Bundler::BundlerError => e 8 | $stderr.puts e.message 9 | $stderr.puts "Run `bundle install` to install missing gems" 10 | exit e.status_code 11 | end 12 | require 'rake' 13 | 14 | require 'rspec/core' 15 | require 'rspec/core/rake_task' 16 | RSpec::Core::RakeTask.new(:spec) do |spec| 17 | spec.pattern = FileList['spec/**/*_spec.rb'] 18 | end 19 | 20 | task :default => :spec 21 | -------------------------------------------------------------------------------- /lib/sith/macro_expander.rb: -------------------------------------------------------------------------------- 1 | require 'parser/current' 2 | require 'unparser' 3 | 4 | module Sith 5 | class MacroExpander 6 | def initialize(macros) 7 | @macros = macros 8 | end 9 | 10 | def expand(source) 11 | ast = Parser::CurrentRuby.parse(source) 12 | expand_node ast 13 | end 14 | 15 | def expand_to_source(source) 16 | Unparser.unparse(expand(source)) 17 | end 18 | 19 | def expand_node(node) 20 | return node unless node.is_a?(Parser::AST::Node) 21 | 22 | if node.type == :send && @macros.key?(node.children[1]) 23 | node = @macros[node.children[1]].expand_macro(node.children[2..-1]) 24 | end 25 | children = node.children.map(&method(:expand_node)) 26 | Parser::AST::Node.new node.type, children 27 | end 28 | end 29 | end 30 | 31 | -------------------------------------------------------------------------------- /sith.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'sith' 6 | s.version = '0.1.0' 7 | s.platform = Gem::Platform::RUBY 8 | s.authors = ["Alexander Ivanov"] 9 | s.email = ["alehander42@gmail.com"] 10 | s.homepage = 'https://github.com/alehander42/sith' 11 | s.summary = %q{A macro preprocessor for Ruby} 12 | s.description = %q{A macro preprocessor for Ruby with ruby-like template notation} 13 | 14 | s.add_development_dependency 'rspec', '~> 0' 15 | s.add_runtime_dependency('parser', '~> 2.2') 16 | s.add_runtime_dependency('unparser', '~> 0.2') 17 | 18 | s.license = 'MIT' 19 | s.executables = ["sith"] 20 | s.files = `git ls-files`.split("\n") 21 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 22 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 23 | s.require_paths = ["lib"] 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Alexander Ivanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sith 2 | 3 | sith is a macro preprocessor for Ruby 4 | 5 | [![Build Status](https://travis-ci.org/alehander42/sith.svg)](https://travis-ci.org/alehander42/sith) 6 | 7 | Still a prototype. 8 | 9 | Example: 10 | 11 | a macro definitions file: 12 | 13 | ```ruby 14 | macro_mapper attr_reader(label) 15 | def ~{label} 16 | @~{label} 17 | end 18 | end 19 | 20 | macro_mapper attr_writer(label) 21 | def ~{label}=(value) 22 | @~{label} = value 23 | end 24 | end 25 | 26 | macro attr_accessor(*labels) 27 | attr_reader ~{labels} 28 | attr_writer ~{labels} 29 | end 30 | ``` 31 | 32 | a ruby file 33 | ``` 34 | class A 35 | attr_accessor a, z 36 | end 37 | ``` 38 | 39 | ```zsh 40 | sith ruby_file.rb macro_definitions.rb > output.rb` 41 | ``` 42 | 43 | output.rb 44 | 45 | ```ruby 46 | class A 47 | def a 48 | @a 49 | end 50 | def z 51 | @z 52 | end 53 | def a=(value) 54 | @a = value 55 | end 56 | def z=(value) 57 | @z = value 58 | end 59 | end 60 | ``` 61 | # install 62 | 63 | `gem install sith` 64 | 65 | # thanks 66 | 67 | built on top of [parser](https://github.com/whitequark/parser) and [unparser](https://github.com/mbj/unparser) gems. 68 | 69 | # similar to 70 | 71 | [rubymacros](https://github.com/coatl/rubymacros/) 72 | 73 | however, the macros in `sith` are defined using a ruby-like template notation, not a lisp-like ast notation. 74 | 75 | # built by 76 | 77 | [Alexander Ivanov](http://alehander42.me) 78 | -------------------------------------------------------------------------------- /lib/sith/loader.rb: -------------------------------------------------------------------------------- 1 | module Sith 2 | def self.load_macros(macro_source) 3 | lines = macro_source.split("\n") 4 | macros = {} 5 | i = 0 6 | while i < lines.length 7 | line = lines[i] 8 | if line.lstrip.start_with? 'macro_mapper' 9 | offset = line.length - line.lstrip.length 10 | a = line.index('(') 11 | label = line[offset + 12...a].strip.to_sym 12 | args = line[a + 1..-1].rstrip[0...-1].split(/[, \"]/) 13 | arg = args[0].to_sym 14 | delimiter = args.length >= 2 ? args[-1] : "\n" 15 | end_index = lines[i..-1].find_index { |l| (l[offset..-1] || '').start_with? 'end' } 16 | body = lines[i + 1...i + end_index].join("\n") 17 | i = i + end_index + 1 18 | macros[label] = MacroMapper.new(arg, delimiter, body) 19 | elsif line.lstrip.start_with? 'macro' 20 | offset = line.length - line.lstrip.length 21 | a = line.index('(') 22 | label = line[offset + 5...a].strip.to_sym 23 | args = line[a + 1..-1].rstrip[0...-1].split(',').map(&:strip) 24 | if args.length == 1 && args[0][0] == '*' 25 | stararg, args = true, [args[0][1..-1].to_sym] 26 | else 27 | stararg, args = false, args.map(&:to_sym) 28 | end 29 | end_index = lines[i..-1].find_index { |l| (l[offset..-1] || '').start_with? 'end' } 30 | body = lines[i + 1...i + end_index].join("\n") 31 | i = i + end_index + 1 32 | macros[label] = Macro.new(args, stararg, body) 33 | else 34 | i += 1 35 | end 36 | end 37 | macros 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/sith/macro.rb: -------------------------------------------------------------------------------- 1 | require 'parser/current' 2 | 3 | module Sith 4 | class BaseMacro 5 | def represent(node) 6 | return node.to_s unless node.is_a?(Parser::AST::Node) || node.is_a?(Array) 7 | 8 | if node.is_a?(Array) 9 | "#{node.map(&method(:represent)).join(', ')}" 10 | elsif node.type == :int 11 | node.children[0].to_s 12 | elsif node.type == :string 13 | node.children[0] 14 | elsif :send 15 | node.children[1].to_s 16 | else 17 | '?' 18 | end 19 | end 20 | 21 | def expand_macro(nodes) 22 | a = expand_to_source(nodes) 23 | 24 | Parser::CurrentRuby.parse a 25 | end 26 | end 27 | 28 | class Macro < BaseMacro 29 | attr_reader :labels, :stararg, :template 30 | 31 | def initialize(labels, stararg=false, template='') 32 | @stararg = stararg 33 | @labels = labels 34 | @template = template 35 | end 36 | 37 | def expand_to_source(nodes) 38 | if @stararg 39 | substitutions = {@labels[0] => represent(nodes)} 40 | else 41 | representations = nodes.map { |node| represent node } 42 | substitutions = Hash[@labels.zip(representations)] 43 | end 44 | @template.gsub(/~\{(\w+)\}/) do |label| 45 | 46 | 47 | substitutions[Regexp.last_match(1).to_sym] 48 | end 49 | end 50 | end 51 | 52 | class MacroMapper < BaseMacro 53 | attr_reader :label, :delimiter, :body 54 | 55 | def initialize(label, delimiter="\n", body='') 56 | @label = label 57 | @delimiter = delimiter 58 | @body = body 59 | end 60 | 61 | def expand_to_source(nodes) 62 | macros = nodes.map { |n| Macro.new([label], false, @body)} 63 | # p macros 64 | macros.zip(nodes).map { |m, n| m.expand_to_source([n]) }.join(delimiter) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/sith_spec.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | require 'rspec' 4 | require 'parser' 5 | require 'sith' 6 | 7 | module Sith 8 | describe Sith do 9 | it 'can load macros from your macros definitions' do 10 | source = <<-RUBY 11 | macro simple(a, b) 12 | ~{a} + ~{b} 13 | end 14 | RUBY 15 | 16 | macros = Sith::load_macros(source) 17 | macro = macros[:simple] 18 | expect(macro).to be_a Macro 19 | expect(macro.labels[0]).to be_a Symbol 20 | expect(macro.labels[0]).to eq :a 21 | expect(macro.template.strip).to eq '~{a} + ~{b}' 22 | end 23 | 24 | it 'can load macro mappers from your macros definitions' do 25 | source = <<-RUBY 26 | macro_mapper attr_reader(attr, delimiter: ";") 27 | def ~{attr} 28 | @~{attr} 29 | end 30 | end 31 | RUBY 32 | 33 | macros = Sith::load_macros(source) 34 | macro = macros[:attr_reader] 35 | expect(macro).to be_a MacroMapper 36 | expect(macro.label).to eq :attr 37 | expect(macro.body.strip).to eq "def ~{attr}\n @~{attr}\n end" 38 | expect(macro.delimiter).to eq ";" 39 | end 40 | 41 | describe 'MacroExpander' do 42 | it 'can expand macros' do 43 | macros = {simple: Macro.new([:a, :b], 44 | false, 45 | '~{a} + ~{b}')} 46 | source = <<-RUBY 47 | a = x + 4 48 | simple(2, 4) 49 | RUBY 50 | expanded = MacroExpander.new(macros).expand(source) 51 | expect(expanded.children[1].type).to eq :send 52 | expect(expanded.children[1].children[2].type).to eq :int 53 | end 54 | 55 | it 'can expand macro mappers' do 56 | macros = {attr_reader: MacroMapper.new(:attr, 57 | "\n", 58 | "def ~{attr};@~{attr};end")} 59 | source = <<-RUBY 60 | class A 61 | attr_reader a 62 | end 63 | RUBY 64 | expanded = MacroExpander.new(macros).expand(source) 65 | expect(expanded.children[2].type).to eq :def 66 | expect(expanded.children[2].children[2].type).to eq :ivar 67 | expect(expanded.children[2].children[2].children[0]).to eq :@a 68 | end 69 | end 70 | end 71 | end 72 | 73 | 74 | 75 | 76 | --------------------------------------------------------------------------------