paintbrush 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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: []