Skip to content

Commit

Permalink
Complete to refactor Colorize
Browse files Browse the repository at this point in the history
  - Add `Colorize::ColorizableIO#colorize_when` with block version.
  - Add `Colorize::IOExtension#to_colorizable` with policy version.
  - Revert `Colorize::StyleBuilder#toggle` for back compatiblity.
  - Add doc-comment more.
  - Add tests for them.
  - More and more...

Breaking changes in this pull request:

  - Remove `inspect` for `Colorize::Object`, because it complicates to
  debug `Colorize::Object` itself. And, we can simply replace
  `with_color.surround { |io| obj.inspect io }` in many case.
  - Change `Colorize::Style#surround` semantics a bit (It is same as old
  `Colorize::Object#surround`.) Its name makes more sense. `push` is
  just internal thing, and it is not important for users.

Only!
  • Loading branch information
makenowjust committed Feb 16, 2017
1 parent eb9679b commit d987041
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 73 deletions.
2 changes: 1 addition & 1 deletion spec/compiler/semantic/generic_class_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ describe "Semantic: generic class" do
semantic(nodes)
rescue ex : TypeException
msg = ex.to_s.lines.map(&.strip)
msg.count(&.includes? "- Foo(T).foo(x : Int32)").should eq(1)
msg.count("- Foo(T).foo(x : Int32)").should eq(1)
end
end

Expand Down
106 changes: 99 additions & 7 deletions spec/std/colorize_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ describe Colorize do
colorize(obj, mode: :bad)
end
end

it "toggles to disable" do
colorize(obj, fore: :red, &.toggle(false)).should eq("")
end

it "toggles to disable, then enable" do
colorize(obj, fore: :red, &.toggle(false).toggle(true)).should eq("\e[31m\e[0m")
end
end
end

Expand All @@ -233,6 +241,19 @@ describe Colorize do
colorizable = original.to_colorizable
colorizable.should be_a(Colorize::ColorizableIO)
colorizable.should_not be(original)
colorizable.should be_a(Colorize::IO)
colorizable.as(Colorize::IO).io.should be(original)
end

it "s default policy is always" do
IO::Memory.new.to_colorizable.colorize_when.should eq(Colorize::When::Always)
end

it "creates a new Colorize::ColorizableIO instance with specified policy" do
original = IO::Memory.new
colorizable = original.to_colorizable(:never)
colorizable.should_not be(original)
colorizable.colorize_when.should eq(Colorize::When::Never)
end

it "returns itself if it is a Colorize::ColorizableIO" do
Expand All @@ -241,6 +262,14 @@ describe Colorize do
colorizable.should be_a(Colorize::ColorizableIO)
colorizable.should be(original)
end

it "returns itself if it is a Colorize::ColorizableIO with original policy" do
original = FakeTTY.new
colorizable = original.to_colorizable(:never)
colorizable.should be_a(Colorize::ColorizableIO)
colorizable.should be(original)
colorizable.colorize_when.should eq(Colorize::When::Always)
end
end
end

Expand All @@ -257,9 +286,29 @@ describe Colorize do
f.colorize_when.should eq(Colorize::When::Auto)
end
end

it "invoke block with specified policy" do
tty = FakeTTY.new
tty.colorize_when(Colorize::When::Never) do |io|
io.colorize_when.should eq(Colorize::When::Never)
end
tty.colorize_when.should eq(Colorize::When::Always)
end
end

describe "#colorize_when=" do
it "sets policies" do
tty = FakeTTY.new
{% for policy in Colorize::When.constants %}
tty.colorize_when = Colorize::When::{{policy}}
tty.colorize_when.should eq(Colorize::When::{{policy}})
tty.colorize_when = {{policy.underscore.stringify}}
tty.colorize_when.should eq(Colorize::When::{{policy}})
tty.colorize_when = {{policy.underscore.symbolize}}
tty.colorize_when.should eq(Colorize::When::{{policy}})
{% end %}
end

it "raises on unknown policy symbol" do
expect_raises ArgumentError, "unknown policy: bad" do
FakeTTY.new.colorize_when = :bad
Expand Down Expand Up @@ -335,34 +384,51 @@ describe Colorize do
it "accepts some objects" do
io = Colorize::Builder.new
(io << "foo" << :foo << 1).should be(io)

io.@contents.size.should eq(1)
io.@contents[0].to_s.should eq("foofoo1")
end

it "accepts Colorize::Object" do
io = Colorize::Builder.new
(io << "foo".colorize.red << "bar".colorize.blue).should be(io)

io.@contents.size.should eq(2)
io.@contents[0].should eq("foo".colorize.red)
io.@contents[1].should eq("bar".colorize.blue)
end

it "accepts mixed objects" do
io = Colorize::Builder.new
(io << "foo".colorize.red << :bar << 42).should be(io)
(io << 1.1 << "foo".colorize.red << :bar << 42).should be(io)

io.@contents.size.should eq(3)
io.@contents[0].to_s.should eq("1.1")
io.@contents[1].should eq("foo".colorize.red)
io.@contents[2].to_s.should eq("bar42")
end
end

describe "#surround" do
it "creates a new builder" do
io = Colorize::Builder.new
io.surround(with_color.red) do |io2|
io2.puts "foo".colorize.bold
io.should_not be(io2)
end
end

it "yields the block with a new builder" do
it "surrounds objects" do
io = Colorize::Builder.new
io.surround(with_color.red) do |io|
puts "foo".colorize.bold
itself.should be(io)
io.surround(with_color.red) do |io2|
io2 << "foo".colorize.bold
end

io.@contents.size.should eq(1)
io.@contents[0].should be_a(Colorize::Object(Colorize::Builder))

io2 = io.@contents[0].as(Colorize::Object(Colorize::Builder)).object
io2.@contents.size.should eq(1)
io2.@contents[0].should eq("foo".colorize.bold)
end
end

Expand All @@ -375,7 +441,7 @@ describe Colorize do

it "outputs Colorize::Object" do
io = Colorize::Builder.new
io << "foo".colorize.red << "bar".colorize.blue
io << "foo".colorize.red << :bar.colorize.blue
io.to_s.should eq("\e[31mfoo\e[0m\e[34mbar\e[0m")
end

Expand All @@ -384,6 +450,32 @@ describe Colorize do
io << "foo".colorize.red << :bar << 42
io.to_s.should eq("\e[31mfoo\e[0mbar42")
end

it "outputs mixed objects, but colorizes dependeing on io" do
io = Colorize::Builder.new
io << "foo".colorize.red << :bar << 42
String.build do |str|
io.to_s str.to_colorizable(:never)
end.should eq("foobar42")
end
end

describe "#to_s_without_colorize" do
it "does not colorize" do
io = Colorize::Builder.new
io << "foo".colorize.red << :bar << 42
io.to_s_without_colorize.should eq("foobar42")
end

it "does not change colorize_when" do
io = Colorize::Builder.new
io << "foo".colorize.red << :bar << 42
mem = IO::Memory.new.to_colorizable(:auto)
mem.colorize_when.should eq(Colorize::When::Auto)
io.to_s_without_colorize mem
mem.io.to_s.should eq("foobar42")
mem.colorize_when.should eq(Colorize::When::Auto)
end
end
end
end
73 changes: 44 additions & 29 deletions src/colorize.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require "colorize/*"

# With `Colorize` you can change the fore- and background colors and text decorations when rendering text
# on terminals supporting ANSI escape codes.
# With `Colorize` you can change the fore- and background colors and text
# decorations when rendering text on terminals supporting ANSI escape codes.
#
# It adds the `colorize` method to `Object` and thus all classes as its main
# interface, which calls `to_s` and surrounds it with the necessary escape
Expand Down Expand Up @@ -86,33 +86,54 @@ require "colorize/*"
# with_color.fore(:yellow).back(:blue).mode(:underline)
# ```
#
# It outputs escape sequences only if target `IO` is TTY.
# You can change this behavior with `always`, `never`, `auto` and `when` methods:
# When `::IO` class have a potential to support ANSI escape sequence, this
# `::IO` class includes `ColorizableIO` module. For example,
# `IO::FileDescriptor` includes `ColorizableIO`.
#
# `ColorizableIO` has `colorize_when` property, which value decides to output escape sequence. If this property's value is:
#
# - `:always` (`When::Always`), it outputs escape sequence always.
# - `:never` (`When::Never`), it doesn't output escape sequence.
# - `:auto` (`When::Auto`), it outputs escape sequence when it is TTY.
#
# `IO::FileDescript#colorize_when`'s default value is `:auto`, so we aren't
# careful the program connects pipe or not.
#
# ```
# # If program connects to pipe (like `crystal run foo.cr | cat`), it
# # doesn't output escape sequence. But if program connects to terminal, it
# # outputs escape sequence.
# puts "foo".colorize.red
#
# # In addition, if IO class doesn't include `ColorizableIO`, it outputs
# # escape sequence as default.
# mem = IO::Memory.new
# mem << "foo".colorize.red
# mem.to_s # => "\e[31mfoo\e[0m"
#
# # Create ColorizableIO from IO object by `to_colorizable` method.
# colorizable_io = IO::Memory.new.to_colorizable
# # Default colorize policy is `Always`.
# colorizable_io.colorize_when # => Colorize::When::Always
# ```
#
# Finally, complete example is:
#
# ```
# # Output colorized `"foo"` if `STDOUT` is TTY (default)
# puts "foo".colorize(fore: :red)
# puts "foo".colorize(fore: :red).auto # explicit
# with_color(fore: :red).surround { puts "foo" }
# with_color(fore: :red).auto.surround { puts "foo" } # explicit
# require "colorize"
#
# # Output colorized `"foo"` even if `STDOUT` is not TTY.
# puts "foo".colorize(fore: :red).always
# with_color(fore: :red).always.surround { puts "foo" }
# # This program outpus escape sequence always
# STDOUT.colorize_when = :always
#
# # Output not colorized `"foo"` even if `STDOUT` is TTY.
# puts "foo".colorize(fore: :red).never
# with_color(fore: :red).never.surround { puts "foo" }
# # Colorize this block as bold.
# with_color.bold.surround do
# print "Hello "
#
# # Alternative ways:
# puts "foo".colorize(fore: :red, when: :always)
# with_color(fore: :red, when: :always).surround { puts "foo" }
# puts "foo".colorize(fore: :red).when(:never)
# with_color(fore: :red).when(:always).surround { puts "foo" }
# # But, colorize only "Crystal" as yellow.
# print "Crystal".colorize.yellow
#
# # Last specified policy is only available.
# puts "foo".colorize.always.auto.never # output no escape sequence.
# with_color(fore: :red).never.auto.always.surround { puts "foo" } # output no escape sequence.
# puts " World!"
# end
# ```
module Colorize
end
Expand All @@ -128,10 +149,4 @@ end

module IO
include Colorize::IOExtension

class FileDescriptor
include Colorize::ColorizableIO

@colorize_when = Colorize::When::Auto
end
end
26 changes: 20 additions & 6 deletions src/colorize/builder.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,47 @@ require "./object"
require "./style"

module Colorize
# Create a new `Builder`, then invoke the block and return this builder.
def self.build
Builder.new.tap do |io|
yield io
end
end

# `Builder` is a `IO` to colorize.
#
# Its `#colorize_when` is always `When::Always` because it delegates given `::IO` on `#to_s` to colorize the output.
#
# It is useful when we cannot decide the output is colorized on creation, for example `Exception` message.
class Builder
include ::IO
include ColorizableIO

@colorize_when = When::Always

# `Builder#colorize_when` is always `When::Always`, so it has no effect.
# `#colorize_when` is always `When::Always`, so it has no effect.
def colorize_when=(policy)
end

# Create a new `Builder`.
def initialize
@contents = Array(Object(String) | Object(Builder) | IO::Memory).new
end

# :nodoc:
def <<(object : Object(String))
@contents << object
self
end

# :nodoc:
def <<(object : Object)
self << Object.new(object.object.to_s).style(object)
end

def surround(style : Style)
@contents << Object.new(Builder.new.tap { |io| with io yield io }).style(style)
# :nodoc:
def surround(style)
@contents << Object.new(Builder.new.tap { |io| yield io }).style(style)
end

# :nodoc:
Expand All @@ -50,18 +59,23 @@ module Colorize
raise "Not implemented"
end

# Output contents to *io*.
#
# Whether to colorize the output depends on *io*'s colorize policy.
def to_s(io)
@contents.each do |content|
io << content
end
end

# Output contents to *io* without color.
def to_s_without_colorize(io)
io = IO.new io
io.colorize_when = When::Never
to_s io
IO.new(io).colorize_when(When::Never) do |io|
to_s io
end
end

# Return contents without color.
def to_s_without_colorize
String.build { |io| to_s_without_colorize io }
end
Expand Down
Loading

0 comments on commit d987041

Please sign in to comment.