paintbrush 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c87f82d4c60332bb80901c9b53ba99d027190044b8b6a8a0d91f0577c870846
4
- data.tar.gz: cb1e9bd1e7c4fc0b65ac0ae6e50ce02dd62e86d7845192b1b14d18a602f069ce
3
+ metadata.gz: 178fc999ac3d70a2f9dc640d1c3a187aac191019a1762af80f922144ab18aeb5
4
+ data.tar.gz: e1f996291bc1748c19ee6389415c896e14f98665b4297cc2217e20fc1657603e
5
5
  SHA512:
6
- metadata.gz: 21bacc0759c545c65c66d70e0fbdb7b406ea1fa224ebcab36e02d9f82ba4e4c0bdbb33dc34f61f52224272ee8c49cae3641d7ca48fdaf0bd5278ae3a3fdf6b08
7
- data.tar.gz: a1340e727fe19c354cb5ae43202afe2ddfbdb94d8748cb8cba16edac3c1d88f3293e6825156c65043329d81f84a78ae041adc6e7d99fad52d64fe8cfe5513ea6
6
+ metadata.gz: 6a05f14e7eb6ce19b86e8301e01000f51651ea01d93470a9a80928e3060e473800434141e537e71343995676c1131dbf69a4c6ecc271f9a2b37f52124fda5cf7
7
+ data.tar.gz: 4f4a5c99d916f055706e366fb7e56bec5a29223d04c604eb04686bcfa6cb596a78a6fdfd685b2a8df3804cf23bb60ce358d9b078d1426fd164b9c5e638d7b954
data/Gemfile CHANGED
@@ -9,6 +9,7 @@ gem 'rake', '~> 13.0'
9
9
 
10
10
  gem 'devpack', '~> 0.4.1'
11
11
  gem 'rspec', '~> 3.0'
12
+ gem 'rspec-documentation', '~> 0.0.2'
12
13
  gem 'rubocop', '~> 1.51'
13
14
  gem 'rubocop-rake', '~> 0.6.0'
14
15
  gem 'rubocop-rspec', '~> 2.22'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- paintbrush (0.1.0)
4
+ paintbrush (0.1.2)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -10,23 +10,38 @@ GEM
10
10
  concurrent-ruby (1.2.2)
11
11
  devpack (0.4.1)
12
12
  diff-lcs (1.5.0)
13
+ htmlbeautifier (1.4.2)
13
14
  i18n (1.13.0)
14
15
  concurrent-ruby (~> 1.0)
15
16
  json (2.6.3)
17
+ kramdown (2.4.0)
18
+ rexml
19
+ kramdown-parser-gfm (1.1.0)
20
+ kramdown (~> 2.0)
16
21
  paint (2.3.0)
17
22
  parallel (1.23.0)
18
23
  parser (3.2.2.1)
19
24
  ast (~> 2.4.1)
20
25
  rainbow (3.1.1)
21
26
  rake (13.0.6)
27
+ redcarpet (3.6.0)
22
28
  regexp_parser (2.8.0)
23
29
  rexml (3.2.5)
30
+ rouge (4.1.1)
24
31
  rspec (3.12.0)
25
32
  rspec-core (~> 3.12.0)
26
33
  rspec-expectations (~> 3.12.0)
27
34
  rspec-mocks (~> 3.12.0)
28
35
  rspec-core (3.12.2)
29
36
  rspec-support (~> 3.12.0)
37
+ rspec-documentation (0.0.2)
38
+ htmlbeautifier (~> 1.4)
39
+ kramdown (~> 2.4)
40
+ kramdown-parser-gfm (~> 1.1)
41
+ paintbrush (~> 0.1.1)
42
+ redcarpet (~> 3.6)
43
+ rouge (~> 4.1)
44
+ rspec (~> 3.12)
30
45
  rspec-expectations (3.12.3)
31
46
  diff-lcs (>= 1.2.0, < 2.0)
32
47
  rspec-support (~> 3.12.0)
@@ -70,6 +85,7 @@ DEPENDENCIES
70
85
  paintbrush!
71
86
  rake (~> 13.0)
72
87
  rspec (~> 3.0)
88
+ rspec-documentation (~> 0.0.2)
73
89
  rubocop (~> 1.51)
74
90
  rubocop-rake (~> 0.6.0)
75
91
  rubocop-rspec (~> 2.22)
data/Rakefile CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'bundler/gem_tasks'
4
4
  require 'rspec/core/rake_task'
5
+ require 'paintbrush'
5
6
 
6
7
  RSpec::Core::RakeTask.new(:spec)
7
8
 
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paintbrush
4
+ # Wraps a Paintbrush::ColorElement instance and maps its start and end boundaries within a
5
+ # compiled escaped string by matching specific unique (indexed) escape codes. Provides
6
+ # `#surround?` for detecting if another element exists within the current element's boundaries.
7
+ class BoundedColorElement
8
+ def initialize(color_element:, escaped_output:)
9
+ @color_element = color_element
10
+ @escaped_output = escaped_output
11
+ end
12
+
13
+ def surround?(element)
14
+ return false if element == self
15
+ return false unless element.open_index.between?(open_index, close_index)
16
+ return false unless element.close_index.between?(open_index, close_index)
17
+
18
+ true
19
+ end
20
+
21
+ def inspect
22
+ "<#{self.class} boundaries=#{boundaries}>"
23
+ end
24
+
25
+ def index
26
+ color_element.index
27
+ end
28
+
29
+ def code
30
+ color_element.code
31
+ end
32
+
33
+ def boundaries
34
+ @boundaries ||= [open_index, close_index]
35
+ end
36
+
37
+ def open_index
38
+ @open_index ||= escaped_output.index(Escapes.open(index))
39
+ end
40
+
41
+ def close_index
42
+ escaped_output.index(Escapes.close(index))
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :escaped_output, :color_element
48
+ end
49
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paintbrush
4
+ # Provides a substring enclosed in unique escape codes for later colorization when the full
5
+ # string has been created and all interpolation is completed. Adds itself to a provided stack
6
+ # of ColorElement objects on initialization.
7
+ class ColorElement
8
+ attr_reader :stack, :code, :string, :index
9
+ attr_accessor :open_index, :close_index
10
+
11
+ def initialize(stack:, code:, string:)
12
+ @stack = stack
13
+ @code = code
14
+ @string = string
15
+ @index = stack.size
16
+ stack << self
17
+ end
18
+
19
+ def to_s
20
+ "#{Escapes.open(index)}#{string}#{Escapes.close(index)}"
21
+ end
22
+
23
+ def inspect
24
+ "<#{self.class.name} index=#{index} code=#{code}>"
25
+ end
26
+ end
27
+ end
@@ -5,44 +5,60 @@ module Paintbrush
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`.
7
7
  class ColorizedString
8
- def initialize(&block)
8
+ def initialize(colorize:, &block)
9
+ @colorize = colorize
9
10
  @block = block
10
- @codes = []
11
+ @stack = []
11
12
  end
12
13
 
13
14
  # Returns a colorized string by injecting escape codes into the various calls to each color
14
15
  # method in the provided block, rebuilds the string and returns the value with regular ANSI
15
16
  # color codes ready to be output to a console.
16
17
  def colorized
17
- colorized_string
18
+ Configuration.with_configuration(colorize: @colorize) do
19
+ colorized_string
20
+ end
18
21
  end
19
22
 
20
23
  private
21
24
 
22
- attr_reader :block, :codes
25
+ attr_reader :block, :stack
26
+
27
+ def colorized_string(string: escaped_output, tree: element_tree)
28
+ tree[:children].reduce(string) do |output, child|
29
+ subbed = subbed_string(output, child[:node], tree[:node])
30
+ next subbed if child[:children].empty?
23
31
 
24
- def colorized_string
25
- codes.each.with_index.reduce(escaped_output) do |string, (code, index)|
26
- restored_color_code = index + 1 == codes.size ? '0' : codes[index + 1]
27
- subbed_string(string, index, code, restored_color_code)
32
+ colorized_string(string: subbed, tree: child)
28
33
  end
29
34
  end
30
35
 
31
- def subbed_string(string, index, code, restored_color_code)
36
+ def bounded_color_elements
37
+ @bounded_color_elements ||= stack.map do |color_element|
38
+ BoundedColorElement.new(color_element: color_element, escaped_output: escaped_output)
39
+ end
40
+ end
41
+
42
+ def element_tree
43
+ @element_tree ||= ElementTree.new(bounded_color_elements: bounded_color_elements).tree
44
+ end
45
+
46
+ def subbed_string(string, color_element, parent_color_element)
47
+ restored_color_code = parent_color_element.nil? ? '0' : parent_color_element.code
32
48
  string
33
- .sub("#{Colors::ESCAPE_START_OPEN}#{index}#{Colors::ESCAPE_START_CLOSE}", "\e[#{code}m")
34
- .sub("#{Colors::ESCAPE_END_OPEN}#{index}#{Colors::ESCAPE_END_CLOSE}", "\e[0m\e[#{restored_color_code}m")
49
+ .sub(Escapes.open(color_element.index).to_s, "\e[#{color_element.code}m")
50
+ .sub(Escapes.close(color_element.index).to_s, "\e[0m\e[#{restored_color_code}m")
35
51
  end
36
52
 
37
53
  def escaped_output
38
- context.instance_eval(&block)
54
+ @escaped_output ||= context.instance_eval(&block)
39
55
  end
40
56
 
41
57
  def context
42
58
  eval('self', block.binding, __FILE__, __LINE__).dup.tap do |context|
43
59
  context.send(:include, Paintbrush::Colors) if context.respond_to?(:include)
44
60
  context.send(:extend, Paintbrush::Colors) if context.respond_to?(:extend)
45
- context.send(:instance_variable_set, :@__codes, codes)
61
+ context.send(:instance_variable_set, :@__stack, stack)
46
62
  end
47
63
  end
48
64
  end
@@ -6,11 +6,6 @@ module Paintbrush
6
6
  # string to be reconstituted afterwards with nested strings restoring the previous color once
7
7
  # they have terminated.
8
8
  module Colors
9
- ESCAPE_START_OPEN = "\e[3;15;17]OPEN:"
10
- ESCAPE_START_CLOSE = "\e[3;15;17]CLOSE:"
11
- ESCAPE_END_OPEN = "\e[17;15;3]OPEN:"
12
- ESCAPE_END_CLOSE = "\e[17;15;3]CLOSE:"
13
-
14
9
  COLOR_CODES = {
15
10
  black: '30',
16
11
  red: '31',
@@ -25,10 +20,11 @@ module Paintbrush
25
20
 
26
21
  COLOR_CODES.each do |name, code|
27
22
  define_method name do |string|
28
- @__codes.push(code)
29
- "#{ESCAPE_START_OPEN}#{@__codes.size - 1}#{ESCAPE_START_CLOSE}" \
30
- "#{string}" \
31
- "#{ESCAPE_END_OPEN}#{@__codes.size - 1}#{ESCAPE_END_CLOSE}"
23
+ if Configuration.colorize?
24
+ ColorElement.new(stack: @__stack, code: code, string: string).to_s
25
+ else
26
+ string
27
+ end
32
28
  end
33
29
  end
34
30
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paintbrush
4
+ # Provides a configuration interface for Paintbrush features, allows disabling colorization.
5
+ #
6
+ # Usage:
7
+ #
8
+ # ```ruby
9
+ # Paintbrush::Configuration.colorize = false
10
+ # ```
11
+ module Configuration
12
+ @defaults = {
13
+ colorize: true
14
+ }
15
+
16
+ @configuration = {}
17
+
18
+ class << self
19
+ attr_reader :configuration, :defaults
20
+
21
+ def colorize=(val)
22
+ configuration[:colorize] = val
23
+ end
24
+
25
+ def colorize?
26
+ configuration.fetch(:colorize, defaults[:colorize])
27
+ end
28
+
29
+ def reset
30
+ @configuration = {}
31
+ end
32
+
33
+ def with_configuration(**options, &block)
34
+ previous = configuration.dup
35
+ options.compact.each { |key, value| configuration[key] = value unless configuration.key?(key) }
36
+ block.call.tap { @configuration = previous }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paintbrush
4
+ # A tree of BoundedColorElement objects. Used to build a full tree of colorized substrings in
5
+ # order to allow discovery of parent substrings and use their color code to restore to when the
6
+ # substring is terminated. Allows deeply-nested colorized strings.
7
+ class ElementTree
8
+ attr_reader :tree
9
+
10
+ def initialize(bounded_color_elements:)
11
+ @bounded_color_elements = bounded_color_elements
12
+ @tree = { node: nil, children: [] }
13
+ build_tree(boundary_end: bounded_color_elements.map(&:close_index).max)
14
+ end
15
+
16
+ def build_tree(boundary_end:, root: @tree, elements: bounded_color_elements, boundary_start: 0)
17
+ root_nodes, non_root_nodes = partitioned_elements(elements, boundary_start, boundary_end)
18
+
19
+ root_nodes.each do |node|
20
+ root[:children] << child_node(node)
21
+ build_tree(
22
+ root: root[:children].last,
23
+ elements: non_root_nodes,
24
+ boundary_start: node.open_index,
25
+ boundary_end: node.close_index
26
+ )
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :bounded_color_elements
33
+
34
+ def child_node(node)
35
+ { node: node, children: [] }
36
+ end
37
+
38
+ def partitioned_elements(elements, boundary_start, boundary_end)
39
+ elements.partition do |element|
40
+ next false if elements.any? { |child| child.surround?(element) }
41
+
42
+ element.boundaries.all? { |index| index.between?(boundary_start, boundary_end) }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Paintbrush
4
+ # Provides an authority on escape code generation. Provides `.close` and `.open`, both of which
5
+ # receive an index (i.e. the current size of the stack). Used for escape code insertion and comparison.
6
+ module Escapes
7
+ def self.open(index)
8
+ "\e[3;15;17]START_OPEN:#{index}:\e[3;15;17]START_CLOSE"
9
+ end
10
+
11
+ def self.close(index)
12
+ "\e[17;15;3]END_OPEN:#{index}:\e[17;15;3]END_CLOSE"
13
+ end
14
+ end
15
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Paintbrush
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.2'
5
5
  end
data/lib/paintbrush.rb CHANGED
@@ -1,8 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'paintbrush/version'
4
+ require_relative 'paintbrush/configuration'
5
+ require_relative 'paintbrush/escapes'
4
6
  require_relative 'paintbrush/colors'
5
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'
6
11
 
7
12
  # Colorizes a string, provides `#paintbrush`. When included/extended in a class, call
8
13
  # `#paintbrush` and pass a block to use the provided dynamically defined methods, e.g.:
@@ -16,9 +21,11 @@ require_relative 'paintbrush/colorized_string'
16
21
  # end
17
22
  # ```
18
23
  module Paintbrush
19
- class Error < StandardError; end
24
+ def self.paintbrush(colorize: nil, &block)
25
+ ColorizedString.new(colorize: colorize, &block).colorized
26
+ end
20
27
 
21
- def paintbrush(&block)
22
- ColorizedString.new(&block).colorized
28
+ def paintbrush(colorize: nil, &block)
29
+ Paintbrush.paintbrush(colorize: colorize, &block)
23
30
  end
24
31
  end
@@ -0,0 +1,36 @@
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
+ ```
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.0
4
+ version: 0.1.2
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-24 00:00:00.000000000 Z
11
+ date: 2023-05-30 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,10 +28,16 @@ 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
31
33
  - lib/paintbrush/colorized_string.rb
32
34
  - lib/paintbrush/colors.rb
35
+ - lib/paintbrush/configuration.rb
36
+ - lib/paintbrush/element_tree.rb
37
+ - lib/paintbrush/escapes.rb
33
38
  - lib/paintbrush/version.rb
34
39
  - paintbrush.gemspec
40
+ - rspec-documentation/pages/Introduction.md
35
41
  - sig/paintbrush.rbs
36
42
  homepage: https://github.com/bobf/paintbrush
37
43
  licenses: []