├── VERSION ├── spec ├── spec.opts ├── examples │ ├── simple.rbe │ ├── no_newline_at_end.rbe │ ├── rescue.rbe │ └── class.rbe ├── spec_helper.rb └── seamless_spec.rb ├── .document ├── .gitignore ├── lib ├── seamless.rb └── seamless │ └── endless.rb ├── LICENSE ├── README.md └── Rakefile /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /spec/examples/simple.rbe: -------------------------------------------------------------------------------- 1 | def testing_method(arg) 2 | arg.to_s + ", " + arg.to_s 3 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /spec/examples/no_newline_at_end.rbe: -------------------------------------------------------------------------------- 1 | class HelloWorlder 2 | def times_two 3 | return "Hello, world! Hello, world!" -------------------------------------------------------------------------------- /spec/examples/rescue.rbe: -------------------------------------------------------------------------------- 1 | module Rescueable 2 | class Rescued 3 | class << self 4 | def a_method 5 | begin 6 | raise "hai" 7 | rescue 8 | return 5 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | 21 | ## PROJECT::SPECIFIC 22 | -------------------------------------------------------------------------------- /spec/examples/class.rbe: -------------------------------------------------------------------------------- 1 | class HelloThing 2 | def initialize(name) 3 | @name = name 4 | 5 | def run_ten_times() 6 | x = 0 7 | result = [] 8 | while x < 10 9 | x += 1 10 | result << "Hello, #{@name}" 11 | result 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | require 'rubygems' 4 | require 'seamless' 5 | require 'spec' 6 | require 'spec/autorun' 7 | 8 | Spec::Runner.configure do |config| 9 | 10 | end 11 | -------------------------------------------------------------------------------- /lib/seamless.rb: -------------------------------------------------------------------------------- 1 | require 'polyglot' 2 | require File.expand_path(File.join(File.dirname(__FILE__), 'seamless', 'endless')) 3 | 4 | class EndlessRubyPolyglotLoader 5 | def self.load(filename, options = nil, &block) 6 | Endless.load(filename) 7 | end 8 | end 9 | 10 | Polyglot.register("rbe", EndlessRubyPolyglotLoader) 11 | -------------------------------------------------------------------------------- /spec/seamless_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | 3 | describe "Seamless" do 4 | def load_example(name) 5 | require File.expand_path(File.dirname(__FILE__) + "/examples/#{name}") 6 | end 7 | 8 | it 'loads a simple method in endless form' do 9 | load_example 'simple' 10 | testing_method(50).should == '50, 50' 11 | end 12 | 13 | it 'loads a class in endless form' do 14 | load_example 'class' 15 | HelloThing.new('mike').run_ten_times.should == ['Hello, mike'] * 10 16 | end 17 | 18 | it 'loads endlessly formed rescues' do 19 | load_example 'rescue' 20 | Rescueable::Rescued.a_method.should == 5 21 | end 22 | 23 | it 'loads endless files with no newline at the end' do 24 | load_example 'no_newline_at_end' 25 | HelloWorlder.new.times_two.should == "Hello, world! Hello, world!" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Michael Edgar 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # seamless 2 | 3 | Python allows you to signal the end of a code block with indentation. Ruby 4 | suffers from an extremely verbose and tedious block terminator, "end". Much 5 | like Lisps end up with dozens of close-parens, Ruby files that use modules 6 | and classes heavily end up with a plethora of "ends" that just aren't necessary. 7 | 8 | Write a Ruby file, but skip all the "ends". Line up your code blocks like in 9 | Python. Then just call it 'your_file.rbe', require 'seamless', and require 10 | 'your_file'. Seamless does the rest. 11 | 12 | Should this ever see widespread use? I don't know. But it's pretty fun! 13 | 14 | ## Example 15 | 16 | module Hello 17 | module World 18 | class Runner 19 | def initialize(user) 20 | @user = user 21 | def run 22 | puts "Hello, #{@user}" 23 | 24 | Much cleaner! Sure, a bit contrived, but no more ends! 25 | 26 | ## Note on Patches/Pull Requests 27 | 28 | * Fork the project. 29 | * Make your feature addition or bug fix. 30 | * Add tests for it. This is important so I don't break it in a 31 | future version unintentionally. 32 | * Commit, do not mess with rakefile, version, or history. 33 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 34 | * Send me a pull request. Bonus points for topic branches. 35 | 36 | ## Copyright 37 | 38 | Copyright (c) 2010 Michael Edgar. See LICENSE for details. 39 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = 'seamless' 8 | gem.summary = %Q{Write ruby without all those 'end's.} 9 | gem.description = %Q{Python allows you to signal the end of a code block 10 | with indentation. Ruby suffers from an extremely verbose and tedious block 11 | terminator, "end". Much like Lisps end up with dozens of close-parens, Ruby 12 | files that use modules and classes heavily end up with a plethora of "ends" 13 | that just aren't necessary. 14 | 15 | Write a Ruby file, but skip all the "ends". Line up your code blocks like in 16 | Python. Then just call it 'your_file.rbe', require 'seamless', and require 17 | 'your_file'. Seamless does the rest.} 18 | gem.email = 'michael.j.edgar@dartmouth.edu' 19 | gem.homepage = 'http://github.com/michaeledgar/seamless' 20 | gem.authors = ['Michael Edgar'] 21 | gem.add_dependency 'polyglot', '>= 0.3.1' 22 | gem.add_dependency 'rubylexer', '>= 0.7.7' 23 | gem.add_development_dependency 'rspec', '~> 1.2' 24 | gem.add_development_dependency 'yard', '>= 0' 25 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 26 | end 27 | Jeweler::GemcutterTasks.new 28 | rescue LoadError 29 | puts 'Jeweler (or a dependency) not available. Install it with: gem install jeweler' 30 | end 31 | 32 | begin 33 | require 'spec/rake/spectask' 34 | Spec::Rake::SpecTask.new(:spec) do |spec| 35 | spec.libs << 'lib' << 'spec' 36 | spec.spec_files = FileList['spec/**/*_spec.rb'] 37 | end 38 | 39 | Spec::Rake::SpecTask.new(:rcov) do |spec| 40 | spec.libs << 'lib' << 'spec' 41 | spec.pattern = 'spec/**/*_spec.rb' 42 | spec.rcov = true 43 | end 44 | task :spec => :check_dependencies 45 | task :default => :spec 46 | rescue LoadError 47 | task :spec do 48 | puts 'RSpec 1.x is required for development. An upgrade to 2.x is coming!' 49 | end 50 | task :default => :spec 51 | end 52 | 53 | 54 | begin 55 | require 'reek/adapters/rake_task' 56 | Reek::RakeTask.new do |t| 57 | t.fail_on_error = true 58 | t.verbose = false 59 | t.source_files = 'lib/**/*.rb' 60 | end 61 | rescue LoadError 62 | task :reek do 63 | abort "Reek is not available. In order to run reek, you must: sudo gem install reek" 64 | end 65 | end 66 | 67 | begin 68 | require 'yard' 69 | YARD::Rake::YardocTask.new 70 | rescue LoadError 71 | task :yardoc do 72 | abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard" 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/seamless/endless.rb: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # Note for seamless: This is from http://gist.github.com/117694, though # 3 | # heavily modified to meet clean code standards and remove some unnecessary # 4 | # functionality. # 5 | ############################################################################# 6 | # 7 | # endless.rb is a pre-processor for ruby which allows you to use python-ish 8 | # indentation to delimit scopes, instead of having to type 'end' every time. 9 | # 10 | # Basically, this makes the end keyword optional. If you leave off the 11 | # end, the preprocessor inserts an end for you at the next line indented 12 | # at or below the level of indentation of the line which started the scope. 13 | # 14 | # End is optional, so you can still write things like this: 15 | # begin 16 | # do_something 17 | # end until done? 18 | # (However, you'd better make damn sure you get the end indented to the 19 | # right level!) 20 | # 21 | # This script uses RubyLexer to extract a stream of tokens and modify 22 | # it, then turn those tokens back into ruby code. Since RubyLexer is a 23 | # complete stand-alone lexer, this should be a very thorough solution, 24 | # free of insoluable little problems due to the script's inability to 25 | # follow where comments and strings start and stop. (That said, I'm sure 26 | # there will be some problems with it, as it's pretty raw code.) 27 | # 28 | # As different programs have a variety of interpretations as to the 29 | # width of a tab character, tabs for indentation are absolutely 30 | # forbidden by endless.rb. 31 | # 32 | # There is a similar script, pyruby.rb, or pyrb.rb floating around which 33 | # examines a source file line by line and assumes lines ending in a colon 34 | # are the start of a block. Since pyrb.rb does not tokenize the input, 35 | # pyruby.rb can be fooled by a colon in a string or comment: 36 | # 37 | # p "a: 38 | # b" 39 | # p "a" #: 40 | # 41 | # It's basically impossible to get this kind of thing right without actually 42 | # tokenizing the input. Also, unlike python (and pyruby.rb), endless.rb needs 43 | # no extra colon to start an indented block. (I don't like python's colons 44 | # much.) 45 | # 46 | # Code written without ends must be pulled into the interpreter via special 47 | # versions of the #load or #eval builtin methods. Eventually, there should 48 | # be a version of #require as well, but for now you must get along with #load. 49 | # The special versions are contained in the Endless module, so to load up 50 | # source code without ends in it, use 51 | # Endless.load 'filename' 52 | # instead of 53 | # require 'filename' 54 | # or 55 | # load 'filename' 56 | 57 | require 'rubylexer' 58 | require 'tempfile' 59 | 60 | class EndlessRubyLexer < RubyLexer 61 | def initialize(*args) 62 | @old_indent_levels=[] 63 | super 64 | end 65 | 66 | def old_indent_level 67 | @old_indent_levels.last 68 | end 69 | 70 | DONT_END = /^( *)(end|when|else|elsif|rescue|ensure)(?!#{LETTER_DIGIT}|[?!])/o 71 | 72 | def start_of_line_directives 73 | super 74 | 75 | if @file.check(/^( *)(.)/) 76 | lm = @file.last_match 77 | @indent_level = lm[1].size if lm[1] 78 | 79 | if lm[2] =~ /\A(?![ ])[\s\v]\Z/ 80 | @indent_level = :invalid 81 | end 82 | 83 | if !(@parsestack.last.respond_to?(:in_body) && !@parsestack.last.in_body) && 84 | (!@moretokens[-2] || NewlineToken === @moretokens[-2]) 85 | #auto-terminate previous same or more indented want-end contexts 86 | pos = input_position 87 | while WantsEndContext===@parsestack.last and 88 | @parsestack.last.indent_level > @indent_level 89 | insert_implicit_end pos 90 | end 91 | while WantsEndContext===@parsestack.last and 92 | @parsestack.last.indent_level == @indent_level 93 | insert_implicit_end pos 94 | end unless @file.check DONT_END 95 | end 96 | end 97 | end 98 | 99 | def insert_implicit_end(pos) 100 | #emit implicit end token 101 | @moretokens.push WsToken.new(' ',pos), 102 | KeywordToken.new('end',pos), 103 | KeywordToken.new(';',pos) 104 | @parsestack.pop 105 | end 106 | 107 | def indent_level 108 | return @indent_level if @indent_level.kind_of?(Integer) 109 | raise "invalid indentation: must use only spaces" 110 | end 111 | 112 | def endoffile_detected(str='') 113 | result = super 114 | pos = input_position 115 | while WantsEndContext === @parsestack.last 116 | insert_implicit_end pos 117 | end 118 | @moretokens.push result 119 | return @moretokens.shift 120 | end 121 | 122 | def keyword_for(str, offset, result) 123 | result = super 124 | @parsestack[-2].indent_level = indent_level 125 | return result 126 | end 127 | 128 | def keyword_def(str, offset, result) 129 | fail unless @indent_level 130 | @old_indent_levels.push @indent_level 131 | result = super 132 | @old_indent_levels.pop 133 | 134 | return result 135 | end 136 | 137 | %w(module class begin case).each do |word| 138 | define_method "keyword_#{word}" do |str, offset, result| 139 | result = super 140 | @parsestack.last.indent_level = indent_level 141 | result 142 | end 143 | end 144 | 145 | %w(while until if unless).each do |word| 146 | define_method "keyword_#{word}" do |str, offset, result| 147 | result = super 148 | if @parsestack[-2].respond_to?(:indent_level=) 149 | @parsestack[-2].indent_level ||= indent_level 150 | end 151 | result 152 | end 153 | end 154 | 155 | def keyword_do(str,offset,result) 156 | result = super 157 | return result if ExpectDoOrNlContext === @parsestack.last 158 | 159 | ctx = @parsestack[-1] 160 | ctx = @parsestack[-2] if BlockParamListLhsContext === ctx 161 | ctx.indent_level = indent_level 162 | return result 163 | end 164 | end 165 | 166 | class RubyLexer::NestedContexts::WantsEndContext 167 | attr_accessor :indent_level 168 | end 169 | 170 | class RubyLexer::NestedContexts::DefContext 171 | alias_method :endful_see, :see 172 | def see(lxr,msg) 173 | if :semi == msg 174 | fail unless lxr.parsestack.last.equal? self 175 | @indent_level ||= lxr.old_indent_level 176 | end 177 | endful_see lxr, msg 178 | end 179 | end 180 | 181 | module Endless 182 | VERSION = "0.1.0" 183 | 184 | class << self 185 | def load(filename,wrap=false) 186 | [''].concat($:).each do |pre| 187 | pre += "/" unless pre.empty? || pre[-2,1] == '/' 188 | path = pre + filename 189 | if File.exist? path 190 | code = File.read(path) + "\n" # force newline at end of file 191 | Tempfile.open(File.basename(filename)) do |f| 192 | preprocess code, filename, f 193 | f.rewind 194 | return ::Kernel::load(f.path, wrap) 195 | end 196 | end 197 | end 198 | raise LoadError, "no such file to load -- #{filename}" 199 | end 200 | 201 | def eval(code, file="(eval)", line=1, binding=nil) 202 | #binding should default to Binding.of_caller, not nil...... 203 | eval(preprocess(code, file), file, line, binding) 204 | end 205 | 206 | def preprocess(code, filename, output='') 207 | lexer = EndlessRubyLexer.new(filename, code) 208 | printer = RubyLexer::KeepWsTokenPrinter.new 209 | 210 | begin 211 | tok = lexer.get1token 212 | output << printer.aprint(tok) 213 | end until RubyLexer::EoiToken === tok 214 | 215 | return output 216 | end 217 | 218 | end 219 | end --------------------------------------------------------------------------------