paintbrush 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/Makefile +9 -0
  4. data/README.md +3 -0
  5. data/lib/paintbrush.rb +18 -9
  6. data/lib/{paintbrush → paintbrush_support}/bounded_color_element.rb +1 -1
  7. data/lib/{paintbrush → paintbrush_support}/color_element.rb +1 -1
  8. data/lib/{paintbrush → paintbrush_support}/colorized_string.rb +3 -3
  9. data/lib/{paintbrush → paintbrush_support}/colors.rb +31 -2
  10. data/lib/{paintbrush → paintbrush_support}/configuration.rb +1 -1
  11. data/lib/{paintbrush → paintbrush_support}/element_tree.rb +1 -1
  12. data/lib/{paintbrush → paintbrush_support}/escapes.rb +1 -1
  13. data/lib/paintbrush_support/hex_color_code.rb +40 -0
  14. data/lib/paintbrush_support/version.rb +5 -0
  15. data/paintbrush.gemspec +3 -2
  16. data/rspec-documentation/pages/000-Introduction.md +23 -0
  17. data/rspec-documentation/pages/010-Colors.md +52 -0
  18. data/rspec-documentation/pages/020-Examples/010-Basic Usage.md +19 -0
  19. data/rspec-documentation/pages/020-Examples/020-Bright Colors.md +15 -0
  20. data/rspec-documentation/pages/020-Examples/030-Nested Colors.md +13 -0
  21. data/rspec-documentation/pages/020-Examples/040-RGB Colors.md +29 -0
  22. data/rspec-documentation/pages/020-Examples/050-Dynamic Usage.md +29 -0
  23. data/rspec-documentation/pages/020-Examples/060-Without Including.md +15 -0
  24. data/rspec-documentation/pages/020-Examples.md +20 -0
  25. data/rspec-documentation/pages/030-Configuration.md +63 -0
  26. data/rspec-documentation/pages/040-How It Works.md +83 -0
  27. data/rspec-documentation/spec_helper.rb +8 -0
  28. metadata +25 -12
  29. data/lib/paintbrush/version.rb +0 -5
  30. data/rspec-documentation/pages/Introduction.md +0 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 178fc999ac3d70a2f9dc640d1c3a187aac191019a1762af80f922144ab18aeb5
4
- data.tar.gz: e1f996291bc1748c19ee6389415c896e14f98665b4297cc2217e20fc1657603e
3
+ metadata.gz: 69b5f9f4807975898e5e3c9ee9a96b183df59a1e57cd642305710479c43e2482
4
+ data.tar.gz: d2b507b2d2ce9a562bdba5066b8d5331faa9f8e1c11dbeb7e75320bb46dac26d
5
5
  SHA512:
6
- metadata.gz: 6a05f14e7eb6ce19b86e8301e01000f51651ea01d93470a9a80928e3060e473800434141e537e71343995676c1131dbf69a4c6ecc271f9a2b37f52124fda5cf7
7
- data.tar.gz: 4f4a5c99d916f055706e366fb7e56bec5a29223d04c604eb04686bcfa6cb596a78a6fdfd685b2a8df3804cf23bb60ce358d9b078d1426fd164b9c5e638d7b954
6
+ metadata.gz: 321564b1aaa6befd52e1b2703b6ac3345824188434f3634f333c39577b1b50fc7517a91c3490a0e34b865b63bb5e55e101ce287fb570a5e22a763c22cf2aa92a
7
+ data.tar.gz: 77e3ab29dd45a4bf28a6b6a1a6e6afdc49414de62b58d801eb5d453167a0d87e359613642ca4b9bcb4345565ae619ba56f0002bdc1c09b46057d669c18a5e003
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- paintbrush (0.1.2)
4
+ paintbrush (0.1.3)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/Makefile CHANGED
@@ -1,5 +1,14 @@
1
+ project=paintbrush
2
+
1
3
  .PHONY: test
2
4
  test:
3
5
  bundle exec rspec
4
6
  bundle exec rubocop
5
7
  bundle exec strong_versions
8
+ bundle exec rspec-documentation
9
+
10
+ .PHONY: publish
11
+ publish:
12
+ @RSPEC_DOCUMENTATION_URL_ROOT='/$(project)' bundle exec rspec-documentation
13
+ @rsync --delete -r rspec-documentation/bundle/ docs01.bob.frl:/mnt/docs/$(project)/
14
+ @echo 'Published.'
data/README.md CHANGED
@@ -42,11 +42,14 @@ Include the `Paintbrush` module anywhere and call `#paintbrush` to generate a co
42
42
  * `#white`
43
43
  * `#default`
44
44
 
45
+ Hex colors are also available as `#hex_ff00ff` and `#hex_f0f`, allowing a much wider range of colors.
46
+
45
47
  Use [string interpolation](https://docs.ruby-lang.org/en/3.2/syntax/literals_rdoc.html#label-String+Literals) to nest multiple colors:
46
48
 
47
49
  ```ruby
48
50
  include Paintbrush
49
51
  puts paintbrush { green "some green text, #{yellow "some yellow text"} and some green again" }
52
+ puts paintbrush { hex_ff00ff "some magenta #{hex_ffff00 "and some yellow"} and magenta again" }
50
53
  ```
51
54
 
52
55
  ## Alternatives
data/lib/paintbrush.rb CHANGED
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'paintbrush/version'
4
- require_relative 'paintbrush/configuration'
5
- require_relative 'paintbrush/escapes'
6
- require_relative 'paintbrush/colors'
7
- require_relative 'paintbrush/colorized_string'
8
- require_relative 'paintbrush/color_element'
9
- require_relative 'paintbrush/bounded_color_element'
10
- require_relative 'paintbrush/element_tree'
3
+ require_relative 'paintbrush_support/version'
4
+ require_relative 'paintbrush_support/configuration'
5
+ require_relative 'paintbrush_support/escapes'
6
+ require_relative 'paintbrush_support/colors'
7
+ require_relative 'paintbrush_support/colorized_string'
8
+ require_relative 'paintbrush_support/color_element'
9
+ require_relative 'paintbrush_support/hex_color_code'
10
+ require_relative 'paintbrush_support/bounded_color_element'
11
+ require_relative 'paintbrush_support/element_tree'
11
12
 
12
13
  # Colorizes a string, provides `#paintbrush`. When included/extended in a class, call
13
14
  # `#paintbrush` and pass a block to use the provided dynamically defined methods, e.g.:
@@ -22,7 +23,15 @@ require_relative 'paintbrush/element_tree'
22
23
  # ```
23
24
  module Paintbrush
24
25
  def self.paintbrush(colorize: nil, &block)
25
- ColorizedString.new(colorize: colorize, &block).colorized
26
+ PaintbrushSupport::ColorizedString.new(colorize: colorize, &block).colorized
27
+ end
28
+
29
+ def self.configure
30
+ yield PaintbrushSupport::Configuration
31
+ end
32
+
33
+ def self.with_configuration(**options, &block)
34
+ PaintbrushSupport::Configuration.with_configuration(**options, &block)
26
35
  end
27
36
 
28
37
  def paintbrush(colorize: nil, &block)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Paintbrush
3
+ module PaintbrushSupport
4
4
  # Wraps a Paintbrush::ColorElement instance and maps its start and end boundaries within a
5
5
  # compiled escaped string by matching specific unique (indexed) escape codes. Provides
6
6
  # `#surround?` for detecting if another element exists within the current element's boundaries.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Paintbrush
3
+ module PaintbrushSupport
4
4
  # Provides a substring enclosed in unique escape codes for later colorization when the full
5
5
  # string has been created and all interpolation is completed. Adds itself to a provided stack
6
6
  # of ColorElement objects on initialization.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Paintbrush
3
+ module PaintbrushSupport
4
4
  # Core string colorization, provides various methods for colorizing a string, uses escape
5
5
  # sequences to store references to start and end of each coloring method to allow nested
6
6
  # colorizing with string interpolation within each individual call to `paintbrush`.
@@ -56,8 +56,8 @@ module Paintbrush
56
56
 
57
57
  def context
58
58
  eval('self', block.binding, __FILE__, __LINE__).dup.tap do |context|
59
- context.send(:include, Paintbrush::Colors) if context.respond_to?(:include)
60
- context.send(:extend, Paintbrush::Colors) if context.respond_to?(:extend)
59
+ context.send(:include, PaintbrushSupport::Colors) if context.respond_to?(:include)
60
+ context.send(:extend, PaintbrushSupport::Colors) if context.respond_to?(:extend)
61
61
  context.send(:instance_variable_set, :@__stack, stack)
62
62
  end
63
63
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Paintbrush
3
+ module PaintbrushSupport
4
4
  # Provides methods that are temporarily injected into block context. Each method returns an
5
5
  # escaped string including the current stack size with start and end escape codes, allowing the
6
6
  # string to be reconstituted afterwards with nested strings restoring the previous color once
@@ -15,9 +15,19 @@ module Paintbrush
15
15
  purple: '35',
16
16
  cyan: '36',
17
17
  white: '37',
18
- default: '39'
18
+ default: '39',
19
+ black_b: '90',
20
+ red_b: '91',
21
+ green_b: '92',
22
+ yellow_b: '93',
23
+ blue_b: '94',
24
+ purple_b: '95',
25
+ cyan_b: '96',
26
+ white_b: '97'
19
27
  }.freeze
20
28
 
29
+ HEX_CODE_REGEXP = /(?:hex_[a-fA-F0-9]{3}){1,2}/.freeze
30
+
21
31
  COLOR_CODES.each do |name, code|
22
32
  define_method name do |string|
23
33
  if Configuration.colorize?
@@ -27,5 +37,24 @@ module Paintbrush
27
37
  end
28
38
  end
29
39
  end
40
+
41
+ def method_missing(method_name, *args)
42
+ return super unless method_name.match?(HEX_CODE_REGEXP)
43
+ return string unless Configuration.colorize?
44
+
45
+ return unless Configuration.colorize?
46
+
47
+ ColorElement.new(
48
+ stack: @__stack,
49
+ code: HexColorCode.new(hex_code: method_name).escape_sequence,
50
+ string: args.first
51
+ ).to_s
52
+ end
53
+
54
+ def respond_to_missing?(*)
55
+ return super unless method_name.match?(HEX_CODE_REGEXP)
56
+
57
+ true
58
+ end
30
59
  end
31
60
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Paintbrush
3
+ module PaintbrushSupport
4
4
  # Provides a configuration interface for Paintbrush features, allows disabling colorization.
5
5
  #
6
6
  # Usage:
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Paintbrush
3
+ module PaintbrushSupport
4
4
  # A tree of BoundedColorElement objects. Used to build a full tree of colorized substrings in
5
5
  # order to allow discovery of parent substrings and use their color code to restore to when the
6
6
  # substring is terminated. Allows deeply-nested colorized strings.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Paintbrush
3
+ module PaintbrushSupport
4
4
  # Provides an authority on escape code generation. Provides `.close` and `.open`, both of which
5
5
  # receive an index (i.e. the current size of the stack). Used for escape code insertion and comparison.
6
6
  module Escapes
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaintbrushSupport
4
+ # Translates a string in format `hex_ff00ff` or `hex_f0f` into an RGB escape code sequence.
5
+ # Allows calling e.g.:
6
+ #
7
+ # paintbrush { hex_ff0ff 'hello in magenta' }
8
+ #
9
+ class HexColorCode
10
+ def initialize(hex_code:)
11
+ @hex_code = hex_code
12
+ end
13
+
14
+ def escape_sequence
15
+ (%w[38 2] + encoded_pairs).join(';')
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :hex_code
21
+
22
+ def encoded_pairs
23
+ pairs.map { |pair| pair.to_i(16).to_s }
24
+ end
25
+
26
+ def pairs
27
+ normalized_hex_string.chars.each_slice(2).map(&:join)
28
+ end
29
+
30
+ def hex_string
31
+ @hex_string ||= hex_code.to_s.partition('hex_').last
32
+ end
33
+
34
+ def normalized_hex_string
35
+ return hex_string if hex_string.size == 6
36
+
37
+ hex_string.chars.map { |char| "#{char}#{char}" }.join
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaintbrushSupport
4
+ VERSION = '0.1.3'
5
+ end
data/paintbrush.gemspec CHANGED
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lib/paintbrush/version'
3
+ require_relative 'lib/paintbrush_support/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'paintbrush'
7
- spec.version = Paintbrush::VERSION
7
+ spec.version = PaintbrushSupport::VERSION
8
8
  spec.authors = ['Bob Farrell']
9
9
  spec.email = ['git@bob.frl']
10
+ spec.licenses = ['MIT']
10
11
 
11
12
  spec.summary = 'Hassle-free text coloring for console applications.'
12
13
  spec.description = 'Provides a set of encapsulated methods for nested colorization of strings.'
@@ -0,0 +1,23 @@
1
+ # Introduction
2
+
3
+ Simple and concise string colorization for _Ruby_ without overloading `String` methods or requiring verbose class/method invocation.
4
+
5
+ _Paintbrush_ has zero dependencies and does not pollute any namespaces or objects outside of the `#paintbrush` method wherever you include the `Paintbrush` module.
6
+
7
+ Nesting is supported, allowing you to use multiple colors within the same string. The previous color is automatically restored.
8
+
9
+ ## Quick Example
10
+
11
+ ```rspec:ansi
12
+ require 'paintbrush'
13
+
14
+ include Paintbrush
15
+
16
+ subject { paintbrush { purple "You used #{green 'four'} #{blue "(#{cyan '4'})"} #{yellow 'colors'} today!" } }
17
+
18
+ it 'outputs simple colorized strings' do
19
+ expect(subject).to eql "\e[35mYou used \e[32mfour\e[0m\e[35m " \
20
+ "\e[34m(\e[36m4\e[0m\e[34m)\e[0m\e[35m " \
21
+ "\e[33mcolors\e[0m\e[35m today!\e[0m\e[0m"
22
+ end
23
+ ```
@@ -0,0 +1,52 @@
1
+ # Colors
2
+
3
+ _Paintbrush_ defines the following colors. Each color is avaiable as a method call inside a block passed to the `paintbrush` method.
4
+
5
+ ## Basic Colors
6
+
7
+ * `black`
8
+ * `red`
9
+ * `green`
10
+ * `yellow`
11
+ * `blue`
12
+ * `purple`
13
+ * `cyan`
14
+ * `white`
15
+ * `default`
16
+
17
+ ```rspec:ansi
18
+ subject { paintbrush { purple 'a purple string' } }
19
+
20
+ it { is_expected.to include "\e[35ma purple string" }
21
+ ```
22
+
23
+ ## Bright Colors
24
+
25
+ Add the `_b` suffix to get the "bright" version of the relevant color.
26
+
27
+ * `black_b`
28
+ * `red_b`
29
+ * `green_b`
30
+ * `yellow_b`
31
+ * `blue_b`
32
+ * `purple_b`
33
+ * `cyan_b`
34
+ * `white_b`
35
+
36
+ ```rspec:ansi
37
+ subject { paintbrush { purple_b 'a bright purple string' } }
38
+
39
+ it { is_expected.to include "\e[95ma bright purple string" }
40
+ ```
41
+
42
+ ## RGB Colors
43
+
44
+ As well as the colors listed below, any RGB hex color code (e.g. `ff00ff`) is available as a method prefixed with `hex_`, e.g. use `hex_ff00ff` for magenta.
45
+
46
+ The shorthand versions `#hex_f0f` are also provided, i.e. `#hex_f0f` is equivalent to `#hex_ff00ff`.
47
+
48
+ ```rspec:ansi
49
+ subject { paintbrush { hex_f0f 'a magenta string' } }
50
+
51
+ it { is_expected.to include "\e[38;2;255;0;255ma magenta string" }
52
+ ```
@@ -0,0 +1,19 @@
1
+ # Basic Usage
2
+
3
+ The most basic usage of _Paintbrush_ is with no colorization at all:
4
+
5
+ ```rspec:ansi
6
+ subject { paintbrush { 'an uncolorized string' } }
7
+
8
+ it { is_expected.to eql 'an uncolorized string' }
9
+ ```
10
+
11
+ As you can see, a duplicate of the original string is returned intact with no modifications.
12
+
13
+ The second-most basic usage of _Paintbrush_ is with a single color:
14
+
15
+ ```rspec:ansi
16
+ subject { paintbrush { green 'some green text' } }
17
+
18
+ it { is_expected.to include 'some green text' }
19
+ ```
@@ -0,0 +1,15 @@
1
+ # Bright Colors
2
+
3
+ Use the `_b` suffix for a color name to make a **bright** color. See [Colors](../colors.html) for more details.
4
+
5
+ ```rspec:ansi
6
+ subject { paintbrush { blue_b "bright blue" } }
7
+
8
+ it { is_expected.to include 'bright blue' }
9
+ ```
10
+
11
+ ```rspec:ansi
12
+ subject { paintbrush { "#{blue "some blue text"} #{blue_b "and some bright blue text"}" } }
13
+
14
+ it { is_expected.to include 'bright blue' }
15
+ ```
@@ -0,0 +1,13 @@
1
+ # Nested Colors
2
+
3
+ _Paintbrush_ supports unlimited levels of nesting, allowing you to use one color inside another color and have the text revert back to its previous color. This lets you create complex colorized strings without having to manually revert back manually.
4
+
5
+ ```rspec:ansi
6
+ subject do
7
+ paintbrush do
8
+ green "green, #{blue "blue, #{cyan "cyan #{yellow "and yellow"}, back to cyan"}, back to blue"}, and back to green"
9
+ end
10
+ end
11
+
12
+ it { is_expected.to include "and yellow" }
13
+ ```
@@ -0,0 +1,29 @@
1
+ # RGB Colors
2
+
3
+ Using _RGB_ colors gives you the full range of over 16 Million colors to use in your terminal. We won't provide an example for each color but here are a few to give you an idea of how it works.
4
+
5
+ ```rspec:ansi
6
+ subject do
7
+ paintbrush do
8
+ "#{hex_f00 'R'}#{hex_ffa500 'A'}#{hex_ff0 'I'}#{hex_080 'N'}" \
9
+ "#{hex_00f 'B'}#{hex_4b0082 'O'}#{hex_ee82ee 'W'}"
10
+ end
11
+ end
12
+
13
+ it 'outputs a rainbow' do
14
+ expect(subject).to eql(
15
+ "\e[38;2;255;0;0mR\e[0m\e[0m\e[38;2;255;165;0mA\e[0m\e[0m" \
16
+ "\e[38;2;255;255;0mI\e[0m\e[0m\e[38;2;0;136;0mN\e[0m\e[0m" \
17
+ "\e[38;2;0;0;255mB\e[0m\e[0m\e[38;2;75;0;130mO\e[0m\e[0m" \
18
+ "\e[38;2;238;130;238mW\e[0m\e[0m"
19
+ )
20
+ end
21
+ ```
22
+
23
+ Note that both the full hex code and the abbreviated versions are supported, i.e. `hex_ff0` produces the same output as `hex_ffff00`:
24
+
25
+ ```rspec:ansi
26
+ subject { paintbrush { hex_ff0 'yellow' } }
27
+
28
+ it { is_expected.to eql(paintbrush { hex_ffff00 'yellow' }) }
29
+ ```
@@ -0,0 +1,29 @@
1
+ # Dynamic Usage
2
+
3
+ You may have situations where the color you wish to use depends on some state that is unknown until your application is running, and may change between each invocation.
4
+
5
+ Since _Paintbrush_ colors are regular methods, you can call `public_send` within the block passed to `paintbrush`.
6
+
7
+
8
+ ```rspec:ansi
9
+ subject do
10
+ paintbrush { blue "The action #{public_send outcome_color, outcome} this time!" }
11
+ end
12
+
13
+ let(:outcome_color) { :green }
14
+ let(:outcome) { 'succeeded' }
15
+
16
+ it { is_expected.to include "\e[32msucceeded\e[0m" }
17
+
18
+ ```
19
+
20
+ ```rspec:ansi
21
+ subject do
22
+ paintbrush { blue "The action #{public_send outcome_color, outcome} this time!" }
23
+ end
24
+
25
+ let(:outcome_color) { :red }
26
+ let(:outcome) { 'failed' }
27
+
28
+ it { is_expected.to include "\e[31mfailed\e[0m" }
29
+ ```
@@ -0,0 +1,15 @@
1
+ # Usage Without `include Paintbrush`
2
+
3
+ _Paintbrush_ is designed to minimize namespace pollution when used with `include`.
4
+
5
+ Only one instance method (`#paintbrush`) is defined on the module, so only one method will be reached by your object's method resolver.
6
+
7
+ _Paintbrush_ also uses a separate namespace for its internals, `PaintbrushSupport`, to minimize adding unwanted constants into an instance's namespace when using `include Paintbrush`.
8
+
9
+ You may still prefer to not include _Paintbrush_ into your namespace. Simply call `Paintbrush.paintbrush` instead.
10
+
11
+ ```rspec:ansi
12
+ subject { Paintbrush.paintbrush { red "I prefer not to #{cyan "include"} Paintbrush" } }
13
+
14
+ it { is_expected.to include "I prefer not" }
15
+ ```
@@ -0,0 +1,20 @@
1
+ # Examples
2
+
3
+ See the individual example pages for usage patterns. Examples are provided to cover basic usage as well as more complex nested colors with multiple color variants. Remember to `include Paintbrush` anywhere you wish to use it.
4
+
5
+ Experiment with _Paintbrush_ from a terminal to get a feel for it:
6
+
7
+ ```irb
8
+ irb(main):001:0> require 'paintbrush'
9
+ => true
10
+ irb(main):002:0> include Paintbrush
11
+ => Object
12
+ irb(main):003:0> puts(paintbrush { green "hello #{cyan 'new'} #{blue 'Paintbrush'} user" })
13
+ hello new Paintbrush user
14
+ ```
15
+
16
+ ```rspec:ansi
17
+ subject { paintbrush { green "hello #{cyan 'new'} #{blue 'Paintbrush'} user" } }
18
+
19
+ it { is_expected.to include 'Paintbrush' }
20
+ ```
@@ -0,0 +1,63 @@
1
+ # Configuration
2
+
3
+ _Paintbrush_ provides one simple configuration option, allowing you to enable or disable colorization either globally, per-invocation, or within a block.
4
+
5
+ Note that global configuration overrides block configuration, and block configuration overrides per-invocation configuration. i.e. any method-level configurations have no impact if a global configuration is set.
6
+
7
+ ## Global Configuration
8
+
9
+ Disable colorization globally by adding the following code somewhere near the beginning of your application's start-up process (e.g. in _Rails_, an initializer is a good place to put this):
10
+
11
+ ```ruby
12
+ # config/initializers/paintbrush.rb
13
+
14
+ Paintbrush.configure do |config|
15
+ config.colorize = false if Rails.env.production?
16
+ end
17
+ ```
18
+
19
+ All calls to `#paintbrush` will now return a regular, uncolorized string.
20
+
21
+ ## Block Configuration
22
+
23
+ _Paintbrush_ can be configured for the duration of a block by calling `Paintbrush.with_configuration`. e.g. you may want to disable colorization in your tests to make testing output a bit easier. If you're using _RSpec_ you can add the following to `spec/spec_helper.rb`:
24
+
25
+ ```ruby
26
+ # spec/spec_helper.rb
27
+
28
+ RSpec.configure do |config|
29
+ config.around { |example| Paintbrush.with_configuration(colorize: false) { example.call } }
30
+ end
31
+ ```
32
+
33
+ ```rspec:ansi
34
+ subject do
35
+ Paintbrush.with_configuration(colorize: false) { paintbrush { red 'An uncolorized string' } }
36
+ end
37
+
38
+ it { is_expected.to eql 'An uncolorized string' }
39
+ ```
40
+
41
+ ## Method Invocation Configuration
42
+
43
+ Pass `colorize: false` to `paintbrush` directly to disable colorization for a single call. This is especially useful when generating a message in two or more contexts. For example, you may want to render the same message to a development log as well as returning that message in a web response, one with colorization and one without:
44
+
45
+ ```rspec:ansi
46
+ def my_message(colorize: true)
47
+ paintbrush(colorize: colorize) { cyan "A log message with colors in some contexts." }
48
+ end
49
+
50
+ subject { my_message(colorize: true) }
51
+
52
+ it { is_expected.to include "\e[36m" }
53
+ ```
54
+
55
+ ```rspec:ansi
56
+ def my_message(colorize: true)
57
+ paintbrush(colorize: colorize) { cyan "A log message with colors in some contexts." }
58
+ end
59
+
60
+ subject { my_message(colorize: false) }
61
+
62
+ it { is_expected.not_to include "\e[36m" }
63
+ ```
@@ -0,0 +1,83 @@
1
+ # How It Works
2
+
3
+ _Paintbrush_ can be broken down into four components:
4
+
5
+ 1. Context manipulation.
6
+ 1. String encoding.
7
+ 1. Color code tree construction.
8
+ 1. Re-encoding into a colorized string.
9
+
10
+ ## Context Manipulation
11
+
12
+ _Paintbrush_ cares about namespace pollution. It avoids adding methods and constants into a namespace where possible, and it does not overload or extend any other objects. _Paintbrush's_ methods are only available within the block passed to the `#paintbrush` method.
13
+
14
+ To achieve this, _Paintbrush_ duplicates the binding of the current block (i.e. the namespace that invoked `#paintbrush`), injects a module `PaintbrushSupport::Colors` (which provides the color methods like `#cyan`) into the duplicated context's `self`, and also adds a `@__stack` instance variable which is unique to each invocation of `#paintbrush`.
15
+
16
+ Each call to `#green`, `#blue`, etc. adds a new `PaintbrushSupport::ColorElement` to the stack, which stores the name of the invoked color method, the string it received, and its index in the current stack.
17
+
18
+ The `ColorElement` object is then returned so _Ruby_ can interpolate it, calling `ColorElement#to_s` which returns an encoded string.
19
+
20
+ ## String Encoding
21
+
22
+ Each encoded string includes the following:
23
+
24
+ * An escape code indicating the beginning of the string.
25
+ * The index of the item in the current stack.
26
+ * The original string received to the color method.
27
+ * An escape code indicating the end of the string.
28
+
29
+ The index is encoded to both the start end end boundaries of the substring, which allows nested colorized strings. The structure is similar to open and close tags in _XML_, with each tag having a unique identifier attribute.
30
+
31
+ The raw encoded string looks like this (slightly formatted to allow text wrapping):
32
+
33
+ ```rspec
34
+ subject do
35
+ PaintbrushSupport::ColorizedString.new(colorize: true) { red "red #{green "green #{blue "blue"}"}" }
36
+ .send(:escaped_output)
37
+ .gsub("CLOSE", "CLOSE ")
38
+ end
39
+
40
+ it { is_expected.to include "green" }
41
+ ```
42
+ This encoded string could be represented in _XML_, for example:
43
+
44
+ ```xml
45
+ <color code="red" id="2">
46
+ red
47
+ <color code="green" id="1">
48
+ green
49
+ <color code="blue" id="0">
50
+ blue
51
+ </color>
52
+ </color>
53
+ </color>
54
+ ```
55
+ However, since the leaf nodes are generated first, and each leaf node does not know where it will appear in the final string, the tree data is built back-to-front and the tree needs to be constructed from the encoded string. Leaves can't attach themselves to parents that don't exist yet, but the resulting string contains information for each node's start/end points and its index in the stack.
56
+
57
+ ## Color Code Tree Construction
58
+
59
+ Once the encoded string has been generated, a tree structure is built by identifying the start and end points of each substring, finding the largest non-overlapping ranges as 1st-generation children, and then repeating the same algorithm using each parent's boundaries to identify direct descendants, until no direct descendants exist (i.e. we have found a leaf node).
60
+
61
+ ## Re-encoding into a colorized string
62
+
63
+ Once the final string has been decoded into a tree structure, each leaf node can inspect its parent to identify which color code should be restored. e.g. if a leaf node has color "yellow" and its parent has color "green", the resulting substring will start with the escape code for yellow, then the string's original value, then the escape code for green.
64
+
65
+ This process is repeated recursively back up the tree until the root is found, at which point the color is reset to the terminal's default.
66
+
67
+ ## Summary
68
+
69
+ The constraints of using string interpolation to create nested colorized strings made developing _Paintbrush_ quite an interesting challenge. Each invocation of a color method receives only the string passed directly to it, and the return value of each method must be an object that _Ruby_ can interpolate into another string, removing the option to pass around objects with complex state and forcing everything to be encoded into the string.
70
+
71
+ The strings received to each call can be stored elsewhere, but the final string structure must define all start and end points of each colorization so that the stack can match each substring to a color code. As each string is received, it does not know where in the final string it will appear.
72
+
73
+ The result is what appears to be a robust model with unlimited nesting. Please [create an issue](https://github.com/bobf/paintbrush/issues) if you are able to break it.
74
+
75
+ ```rspec:ansi
76
+ subject do
77
+ paintbrush do
78
+ "nesting with #{blue 'foo'} #{green "bar #{cyan "baz"} with #{cyan 'qux'} and quux"} and #{red "corge"}"
79
+ end
80
+ end
81
+
82
+ it { is_expected.to include 'baz' }
83
+ ```
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec::Documentation.configure do |config|
4
+ config.context do
5
+ require 'paintbrush'
6
+ include Paintbrush
7
+ end
8
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paintbrush
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-30 00:00:00.000000000 Z
11
+ date: 2023-06-04 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Provides a set of encapsulated methods for nested colorization of strings.
14
14
  email:
@@ -28,19 +28,32 @@ files:
28
28
  - Rakefile
29
29
  - doc/example.png
30
30
  - lib/paintbrush.rb
31
- - lib/paintbrush/bounded_color_element.rb
32
- - lib/paintbrush/color_element.rb
33
- - lib/paintbrush/colorized_string.rb
34
- - lib/paintbrush/colors.rb
35
- - lib/paintbrush/configuration.rb
36
- - lib/paintbrush/element_tree.rb
37
- - lib/paintbrush/escapes.rb
38
- - lib/paintbrush/version.rb
31
+ - lib/paintbrush_support/bounded_color_element.rb
32
+ - lib/paintbrush_support/color_element.rb
33
+ - lib/paintbrush_support/colorized_string.rb
34
+ - lib/paintbrush_support/colors.rb
35
+ - lib/paintbrush_support/configuration.rb
36
+ - lib/paintbrush_support/element_tree.rb
37
+ - lib/paintbrush_support/escapes.rb
38
+ - lib/paintbrush_support/hex_color_code.rb
39
+ - lib/paintbrush_support/version.rb
39
40
  - paintbrush.gemspec
40
- - rspec-documentation/pages/Introduction.md
41
+ - rspec-documentation/pages/000-Introduction.md
42
+ - rspec-documentation/pages/010-Colors.md
43
+ - rspec-documentation/pages/020-Examples.md
44
+ - rspec-documentation/pages/020-Examples/010-Basic Usage.md
45
+ - rspec-documentation/pages/020-Examples/020-Bright Colors.md
46
+ - rspec-documentation/pages/020-Examples/030-Nested Colors.md
47
+ - rspec-documentation/pages/020-Examples/040-RGB Colors.md
48
+ - rspec-documentation/pages/020-Examples/050-Dynamic Usage.md
49
+ - rspec-documentation/pages/020-Examples/060-Without Including.md
50
+ - rspec-documentation/pages/030-Configuration.md
51
+ - rspec-documentation/pages/040-How It Works.md
52
+ - rspec-documentation/spec_helper.rb
41
53
  - sig/paintbrush.rbs
42
54
  homepage: https://github.com/bobf/paintbrush
43
- licenses: []
55
+ licenses:
56
+ - MIT
44
57
  metadata:
45
58
  homepage_uri: https://github.com/bobf/paintbrush
46
59
  source_code_uri: https://github.com/bobf/paintbrush
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Paintbrush
4
- VERSION = '0.1.2'
5
- end
@@ -1,36 +0,0 @@
1
- # Paintbrush
2
-
3
- Simple and concise string colorization for _Ruby_ without overloading `String` methods or requiring verbose class/method invocation.
4
-
5
- _Paintbrush_ has zero dependencies and does not pollute any namespaces or objects outside of the `#paintbrush` method wherever you include the `Paintbrush` module.
6
-
7
- Nesting is supported, allowing you to use multiple colors within the same string. The previous color is automatically restored.
8
-
9
- ```rspec:ansi
10
- require 'paintbrush'
11
-
12
- extend Paintbrush
13
-
14
- output = paintbrush { purple "You used #{green 'four'} #{blue "(#{cyan '4'})"} #{yellow 'colors'} today!" }
15
- it_documents output do
16
- expect(output).to eql "\e[35mYou used \e[32mfour\e[0m\e[35m " \
17
- "\e[34m(\e[36m4\e[0m\e[34m)\e[0m\e[35m " \
18
- "\e[33mcolors\e[0m\e[35m today!\e[0m\e[0m"
19
- end
20
- ```
21
-
22
- ```rspec:ansi
23
- require 'paintbrush'
24
-
25
- extend Paintbrush
26
-
27
- output = paintbrush do
28
- "#{blue 'foo'} #{green "bar #{cyan %w[foo bar baz].join(', ')} with #{cyan 'qux'} and quux"} and corge"
29
- end
30
-
31
- it_documents output do
32
- expect(output).to eql "\e[34mfoo\e[0m\e[0m \e[32mbar \e[36mfoo, bar, baz" \
33
- "\e[0m\e[32m with \e[36mqux\e[0m\e[32m "\
34
- "and quux\e[0m\e[0m and corge"
35
- end
36
- ```