Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

defines String#indent [closes #7263] [Xavier Noria & Ace Suares]

  • Loading branch information...
commit 2f58795e783150f2e1b1f6c64e305703f0061129 1 parent 9cd1f69
@fxn fxn authored
View
2  activesupport/CHANGELOG.md
@@ -1,5 +1,7 @@
## Rails 4.0.0 (unreleased) ##
+* Add String#indent. *fxn & Ace Suares*
+
* Inflections can now be defined per locale. `singularize` and `pluralize` accept locale as an extra argument. *David Celis*
* `Object#try` will now return nil instead of raise a NoMethodError if the receiving object does not implement the method, but you can still get the old behavior by using the new `Object#try!` *DHH*
View
1  activesupport/lib/active_support/core_ext/string.rb
@@ -10,3 +10,4 @@
require 'active_support/core_ext/string/exclude'
require 'active_support/core_ext/string/strip'
require 'active_support/core_ext/string/inquiry'
+require 'active_support/core_ext/string/indent'
View
43 activesupport/lib/active_support/core_ext/string/indent.rb
@@ -0,0 +1,43 @@
+class String
+ # Same as +indent+, except it indents the receiver in-place.
+ #
+ # Returns the indented string, or +nil+ if there was nothing to indent.
+ def indent!(amount, indent_string=nil, indent_empty_lines=false)
+ indent_string = indent_string || self[/^[ \t]/] || ' '
+ re = indent_empty_lines ? /^/ : /^(?!$)/
+ gsub!(re, indent_string * amount)
+ end
+
+ # Indents the lines in the receiver:
+ #
+ # <<EOS.indent(2)
+ # def some_method
+ # some_code
+ # end
+ # EOS
+ # # =>
+ # def some_method
+ # some_code
+ # end
+ #
+ # The second argument, +indent_string+, specifies which indent string to
+ # use. The default is +nil+, which tells the method to make a guess by
+ # peeking at the first indented line, and fallback to a space if there is
+ # none.
+ #
+ # " foo".indent(2) # => " foo"
+ # "foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar"
+ # "foo".indent(2, "\t") # => "\t\tfoo"
+ #
+ # While +indent_string+ is tipically one space or tab, it may be any string.
+ #
+ # The third argument, +indent_empty_lines+, is a flag that says whether
+ # empty lines should be indented. Default is false.
+ #
+ # "foo\n\nbar".indent(2) # => " foo\n\n bar"
+ # "foo\n\nbar".indent(2, nil, true) # => " foo\n \n bar"
+ #
+ def indent(amount, indent_string=nil, indent_empty_lines=false)
+ dup.tap {|_| _.indent!(amount, indent_string, indent_empty_lines)}
+ end
+end
View
56 activesupport/test/core_ext/string_ext_test.rb
@@ -9,6 +9,7 @@
require 'active_support/time'
require 'active_support/core_ext/string/strip'
require 'active_support/core_ext/string/output_safety'
+require 'active_support/core_ext/string/indent'
module Ace
module Base
@@ -521,3 +522,58 @@ class StringExcludeTest < ActiveSupport::TestCase
assert_equal true, 'foo'.exclude?('p')
end
end
+
+class StringIndentTest < ActiveSupport::TestCase
+ test 'does not indent strings that only contain newlines (edge cases)' do
+ ['', "\n", "\n" * 7].each do |str|
+ assert_nil str.indent!(8)
+ assert_equal str, str.indent(8)
+ assert_equal str, str.indent(1, "\t")
+ end
+ end
+
+ test "by default, indents with spaces if the existing indentation uses them" do
+ assert_equal " foo\n bar", "foo\n bar".indent(4)
+ end
+
+ test "by default, indents with tabs if the existing indentation uses them" do
+ assert_equal "\tfoo\n\t\t\bar", "foo\n\t\bar".indent(1)
+ end
+
+ test "by default, indents with spaces as a fallback if there is no indentation" do
+ assert_equal " foo\n bar\n baz", "foo\nbar\nbaz".indent(3)
+ end
+
+ # Nothing is said about existing indentation that mixes spaces and tabs, so
+ # there is nothing to test.
+
+ test 'uses the indent char if passed' do
+ assert_equal <<EXPECTED, <<ACTUAL.indent(4, '.')
+.... def some_method(x, y)
+.... some_code
+.... end
+EXPECTED
+ def some_method(x, y)
+ some_code
+ end
+ACTUAL
+
+ assert_equal <<EXPECTED, <<ACTUAL.indent(2, '&nbsp;')
+&nbsp;&nbsp;&nbsp;&nbsp;def some_method(x, y)
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;some_code
+&nbsp;&nbsp;&nbsp;&nbsp;end
+EXPECTED
+&nbsp;&nbsp;def some_method(x, y)
+&nbsp;&nbsp;&nbsp;&nbsp;some_code
+&nbsp;&nbsp;end
+ACTUAL
+ end
+
+ test "does not indent blank lines by default" do
+ assert_equal " foo\n\n bar", "foo\n\nbar".indent(1)
+ end
+
+ test 'indents blank lines if told so' do
+ assert_equal " foo\n \n bar", "foo\n\nbar".indent(1, nil, true)
+ end
+end
View
35 guides/source/active_support_core_extensions.textile
@@ -1325,6 +1325,41 @@ that amount of leading whitespace.
NOTE: Defined in +active_support/core_ext/string/strip.rb+.
+h4. +indent+
+
+Indents the lines in the receiver:
+
+<ruby>
+<<EOS.indent(2)
+def some_method
+ some_code
+end
+EOS
+# =>
+ def some_method
+ some_code
+ end
+</ruby>
+
+The second argument, +indent_string+, specifies which indent string to use. The default is +nil+, which tells the method to make an educated guess peeking at the first indented line, and fallback to a space if there is none.
+
+<ruby>
+" foo".indent(2) # => " foo"
+"foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar"
+"foo".indent(2, "\t") # => "\t\tfoo"
+</ruby>
+
+While +indent_string+ is tipically one space or tab, it may be any string.
+
+The third argument, +indent_empty_lines+, is a flag that says whether empty lines should be indented. Default is false.
+
+<ruby>
+"foo\n\nbar".indent(2) # => " foo\n\n bar"
+"foo\n\nbar".indent(2, nil, true) # => " foo\n \n bar"
+</ruby>
+
+The +indent!+ method performs indentation in-place.
+
h4. Access
h5. +at(position)+

6 comments on commit 2f58795

@acesuares

Excellent! I leared a lot, like self[/^[ \t]/] and /^(?!$)/.

So, the way I would use it, is strip_heredoc.indent(2), where strip_heredoc would leave the first line without any space or tab, and indent(2) would add spaces (the default) or I would have to specify '\t' as second argument. Great! and the extra flag for empty lines, also nice ot have.

@acesuares

Thx for the credits (feel I hardly deserve them). And you closed the pull request by a commit, so now it's in rails? Wow.

@acesuares

Hmm the only thing I don't get is the ! in /^(?!$)/

@fxn
Owner

@acesuares exactly, as per the origin of the ticket you'd write

class C
  def some_method
    <<-EOS.strip_heredoc.indent(2)
      cofing.foo = :bar
      config.baz = :zoo
    EOS
  end
end

Regarding the regexp, as you surely know ^ is an anchor that matches start of string or beginning of line. In some programming languages a regexp modifier is needed to match beginning of line, Perl is one of them, but not in Ruby.

Then (?!...) says "and from this point on this should not match", where "this" is the regexp following the exclamation mark up to the closing paren. For example, /a(?!e)/ matches any a that is not followed by an e. That is technically called a negative look-ahead assertion. Our regexp in that negative assertion is very simple: $, which is just an anchor that matches at the end of line, optionally matching also the newline, or end of string.

So the whole regexp says: "please match at the beginning of any line that has something else than a newline or end of the string right after that", which is a fancy way to say "please match at the beginning of any line that is not empty" (a line is empty if consists only of the newline character or is an empty string).

Note that we match empty lines. In some contexts you also want to accept blank lines, that is, lines which are either empty or contain only whitespace. But in this case I opted to support only empty lines, because if a line already has 4 spaces, getting 6 or 4 after the indent is not big deal. So what we skip by default are empty lines, which is what you normally want if indenting source code.

@steveklabnik
Collaborator

It's important to note that Ruby's $ works differently than in other languages' regular expressions, so take care!

@fxn
Owner

Yeah, the same remark of ^ applies to $. In some languages you need to put a modifier to match end of line, but not in Ruby (that's why there's none in the regexps of this patch).

Please sign in to comment.
Something went wrong with that request. Please try again.