eskimo 1.0.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5912c4b1987656f45fa2fc0beb17228f1cc967db41c084675774baf24167bb9f
4
+ data.tar.gz: d8f3d3d02a29fabd13444207a972e3f92e0403d1ff5612d4fc40876e1dc349d6
5
+ SHA512:
6
+ metadata.gz: fa006a58bbd5ad12d24e52a3401ed14d7ecc8a98cc5e494856dcba95666df4ff3be54eab24902e8167d687f7e6d8daca5e3d33da90ddf67efdce6cc2e87cab01
7
+ data.tar.gz: 4c249f2b9b14cc4521b516ba78c0f1bbbbf7454600888a4eaab4ce55492f9e51447e48897be98d47c1d639b0a41fc70c2febbed6be955a0d978bcebcd2592f87
@@ -0,0 +1,10 @@
1
+ require 'pastel'
2
+ require 'strings'
3
+ require 'tty-screen'
4
+
5
+ require_relative './eskimo/version'
6
+
7
+ require_relative './eskimo/component'
8
+ require_relative './eskimo/renderer'
9
+
10
+ require_relative './eskimo/components'
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eskimo
4
+ # A base component class that renders child components defined in a block into
5
+ # a String for further formatting.
6
+ #
7
+ # class MyComponent < Eskimo::Component
8
+ # def render(**)
9
+ # text = super
10
+ # text.is_a?(String) # => true
11
+ # end
12
+ # end
13
+ #
14
+ # Use of this class is optional. What's happening under the hood is:
15
+ #
16
+ # 1. Component maintains a reference to the Proc passed to {#initialize}. That
17
+ # Proc can potentially return a list of child components to render.
18
+ #
19
+ # 2. {Component#render} (called via "super" from sub-classes) invokes the
20
+ # `render` prop provided by {Renderer#apply} with the tracked children
21
+ # which converts them to a String and returns them.
22
+ class Component
23
+ def initialize(&children)
24
+ @children = children
25
+ end
26
+
27
+ def render(render:, **)
28
+ render[@children]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eskimo
4
+ module Components
5
+ SCREEN_COLUMNS = [TTY::Screen.width, 72].min
6
+
7
+ # Indent text from the left.
8
+ class Indent < Component
9
+ attr_reader :width
10
+
11
+ def initialize(width: 4, &children)
12
+ @width = width
13
+ super(&children)
14
+ end
15
+
16
+ def render(**)
17
+ Strings.pad(super, [0,0,0,width]).rstrip
18
+ end
19
+ end
20
+
21
+ # Insert a hard line-break (paragraph-like.)
22
+ class LineBreak
23
+ def render(**)
24
+ "\n \n"
25
+ end
26
+ end
27
+
28
+ # Insert a soft line-break.
29
+ class SoftBreak
30
+ def render(**)
31
+ "\n"
32
+ end
33
+ end
34
+
35
+ # Strip text of surrounding whitespace.
36
+ class Strip < Component
37
+ def render(**)
38
+ super.strip
39
+ end
40
+ end
41
+
42
+ # Style text with colors and custom formatting.
43
+ #
44
+ # See [Pastel's documentation][pastel] for the accepted styles.
45
+ #
46
+ # [pastel]: https://github.com/piotrmurach/pastel
47
+ class Style < Component
48
+ attr_reader :pastel, :style
49
+
50
+ def initialize(*style, &children)
51
+ @style = style.flatten
52
+ @pastel = Pastel.new
53
+
54
+ super(&children)
55
+ end
56
+
57
+ def render(**)
58
+ pastel.decorate(super, *style)
59
+ end
60
+ end
61
+
62
+ # Truncate text from the beginning if it exceeds a certain width.
63
+ #
64
+ # ...bar
65
+ class Truncate < Component
66
+ attr_reader :maxlen
67
+
68
+ def initialize(reserve: 0, width: SCREEN_COLUMNS, &children)
69
+ @maxlen = [0, width - reserve].max
70
+
71
+ super(&children)
72
+ end
73
+
74
+ def render(**)
75
+ text = super
76
+
77
+ if text.length >= maxlen
78
+ '...' + text[text.length - maxlen - 1 .. -1]
79
+ else
80
+ text
81
+ end
82
+ end
83
+ end
84
+
85
+ # Truncate text from the rear if it exceeds a certain width.
86
+ #
87
+ # foo...
88
+ class TruncateRear < Truncate
89
+ def render(render:, **)
90
+ text = render[@children]
91
+
92
+ if text.length >= maxlen
93
+ text[0..maxlen - 1] + '...'
94
+ else
95
+ text
96
+ end
97
+ end
98
+ end
99
+
100
+ # Wrap a block text with newlines at a certain threshold.
101
+ class Wrap < Component
102
+ attr_reader :width
103
+
104
+ def initialize(width: SCREEN_COLUMNS, &children)
105
+ @width = width
106
+ super(&children)
107
+ end
108
+
109
+ def render(**)
110
+ Strings.wrap(super, width)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eskimo
4
+ # Render a component or a list of ones.
5
+ class Renderer
6
+ # @param props [Hash]
7
+ # Properties to pass to each component being rendered.
8
+ def initialize(**props)
9
+ @props = { **props, render: method(:render) }
10
+ end
11
+
12
+ # @param components [Proc]
13
+ # A block that returns components to render.
14
+ def apply(&components)
15
+ render(components)
16
+ end
17
+
18
+ private
19
+
20
+ def render(*components)
21
+ components.select(&NOT_FALSEY).reduce('') do |buf, component|
22
+ case component
23
+ when String
24
+ buf + component
25
+ when Array
26
+ buf + render(*component)
27
+ when Proc
28
+ buf + render(component[**@props])
29
+ else
30
+ if component.respond_to?(:render)
31
+ buf + render(component.render(**@props))
32
+ else
33
+ bail(component)
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def bail(component)
40
+ raise ArgumentError.new(
41
+ "Eskimo: don't know how to render #{component.class} => #{component}"
42
+ )
43
+ end
44
+
45
+ NOT_FALSEY = ->(x) { !!x }.freeze
46
+ private_constant :NOT_FALSEY
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Eskimo
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Eskimo::Renderer do
6
+ ESK = Eskimo::Components
7
+
8
+ describe 'SOFT_BREAK' do
9
+ it 'inserts a soft linebreak' do
10
+ expect(
11
+ subject.apply {
12
+ [
13
+ 'hello',
14
+ ESK::SoftBreak.new,
15
+ 'world'
16
+ ]
17
+ }
18
+ ).to eq("hello\nworld")
19
+ end
20
+ end
21
+
22
+ describe 'BR' do
23
+ it 'inserts a hard linebreak' do
24
+ expect(
25
+ subject.apply {
26
+ [
27
+ 'hello',
28
+ ESK::LineBreak.new,
29
+ 'world'
30
+ ]
31
+ }
32
+ ).to eq("hello\n \nworld")
33
+ end
34
+ end
35
+
36
+ describe 'STYLE' do
37
+ it 'applies ANSI styling to text' do
38
+ expect(
39
+ subject.apply {
40
+ [
41
+ ESK::Style.new([:bold, :green]) { 'hai' },
42
+ ]
43
+ }
44
+ ).to eq("\e[1;32mhai\e[0m")
45
+ end
46
+
47
+ it 'accepts a splat' do
48
+ expect(
49
+ subject.apply {
50
+ [
51
+ ESK::Style.new(:bold, :green) { 'hai' },
52
+ ]
53
+ }
54
+ ).to eq("\e[1;32mhai\e[0m")
55
+ end
56
+ end
57
+
58
+ describe 'WRAP' do
59
+ it 'wraps text' do
60
+ expect(
61
+ subject.apply do
62
+ ESK::Wrap.new(width: 5) do
63
+ 'hello world'
64
+ end
65
+ end
66
+ ).to eq("\nhello \nworld")
67
+ end
68
+ end
69
+
70
+ describe 'INDENT' do
71
+ it 'indents each line' do
72
+ expect(
73
+ subject.apply do
74
+ ESK::Indent.new(width: 2) do
75
+ "hello\nworld"
76
+ end
77
+ end
78
+ ).to eq(" hello\n world")
79
+ end
80
+ end
81
+
82
+ describe 'STRIP' do
83
+ it 'strips surrounding whitespace' do
84
+ expect(
85
+ subject.apply do
86
+ ESK::Strip.new do
87
+ ESK::Indent.new(width: 2) do
88
+ ("hello world")
89
+ end
90
+ end
91
+ end
92
+ ).to eq("hello world")
93
+ end
94
+ end
95
+
96
+ describe 'TRUNCATE' do
97
+ it 'truncates a string from the beginning if it exceeds the length' do
98
+ expect(
99
+ subject.apply do
100
+ ESK::Truncate.new(width: 5) do
101
+ ("hello world")
102
+ end
103
+ end
104
+ ).to eq("... world")
105
+ end
106
+
107
+ it 'reserves chars from the width' do
108
+ expect(
109
+ subject.apply do
110
+ ESK::Truncate.new(width: 10, reserve: 5) do
111
+ ("hello world")
112
+ end
113
+ end
114
+ ).to eq("... world")
115
+ end
116
+
117
+ it 'is a no-op if text fits' do
118
+ expect(
119
+ subject.apply do
120
+ ESK::Truncate.new(width: 62, reserve: 5) do
121
+ ("hello world")
122
+ end
123
+ end
124
+ ).to eq("hello world")
125
+ end
126
+ end
127
+
128
+ describe 'TruncateRear' do
129
+ it 'truncates a string from the rear if it exceeds the length' do
130
+ expect(
131
+ subject.apply do
132
+ ESK::TruncateRear.new(width: 5) do
133
+ "hello world"
134
+ end
135
+ end
136
+ ).to eq("hello...")
137
+ end
138
+
139
+ it 'reserves chars from the width' do
140
+ expect(
141
+ subject.apply do
142
+ ESK::TruncateRear.new(width: 10, reserve: 5) do
143
+ ("hello world")
144
+ end
145
+ end
146
+ ).to eq("hello...")
147
+ end
148
+
149
+ it 'is a no-op if text fits' do
150
+ expect(
151
+ subject.apply do
152
+ ESK::TruncateRear.new(width: 62, reserve: 5) do
153
+ ("hello world")
154
+ end
155
+ end
156
+ ).to eq("hello world")
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Eskimo::Renderer do
6
+ it 'renders nothing' do
7
+ expect(subject.apply).to eq('')
8
+ end
9
+
10
+ it 'renders a string' do
11
+ expect(subject.apply { 'hello' }).to eq('hello')
12
+ end
13
+
14
+ it 'renders a proc' do
15
+ expect(subject.apply { lambda do |**| 'hello' end }).to eq('hello')
16
+ end
17
+
18
+ it 'renders an array of renderables' do
19
+ expect(subject.apply { ['h','ello'] }).to eq('hello')
20
+ end
21
+
22
+ it 'renders a #render renderable' do
23
+ class Renderable
24
+ def render(**)
25
+ 'hello'
26
+ end
27
+ end
28
+
29
+ expect(subject.apply { Renderable.new }).to eq('hello')
30
+ end
31
+
32
+ it 'ignores falseys' do
33
+ expect(subject.apply { nil }).to eq('')
34
+ expect(subject.apply { false }).to eq('')
35
+ expect(subject.apply { [nil,false] }).to eq('')
36
+ end
37
+
38
+ it 'passes props to renderers' do
39
+ props = nil
40
+
41
+ described_class.new(foo: 'bar').apply {
42
+ lambda { |injected_props| ''.tap { props = injected_props } }
43
+ }
44
+
45
+ expect(props).to be_a(Hash)
46
+ expect(props).to include(foo: 'bar')
47
+ end
48
+
49
+ it 'injects a "render" routine for procs' do
50
+ expect(
51
+ subject.apply { lambda { |render:, **| render['hello'] } }
52
+ ).to eq('hello')
53
+ end
54
+
55
+ it 'bails on unrenderable inputs' do
56
+ expect {
57
+ subject.apply { :foo }
58
+ }.to raise_error(ArgumentError,
59
+ "Eskimo: don't know how to render #{:foo.class} => foo"
60
+ )
61
+ end
62
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'simplecov'
4
+
5
+ SimpleCov.start do
6
+ add_filter(%r{/spec/})
7
+ coverage_dir("#{ENV.fetch('COVERAGE_DIR', 'coverage')}")
8
+ end
9
+
10
+ require 'eskimo'
11
+
12
+ RSpec.configure do |config|
13
+ config.expect_with :rspec do |expectations|
14
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
15
+ end
16
+
17
+ config.mock_with :rspec do |mocks|
18
+ mocks.verify_partial_doubles = true
19
+ end
20
+
21
+ config.shared_context_metadata_behavior = :apply_to_host_groups
22
+
23
+ config.filter_run_when_matching :focus
24
+
25
+ config.disable_monkey_patching!
26
+
27
+ config.warnings = true
28
+
29
+ config.default_formatter = 'doc' if config.files_to_run.one?
30
+
31
+ config.order = :random
32
+
33
+ Kernel.srand config.seed
34
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eskimo
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ahmad Amireh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-01-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pastel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: strings
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: tty-screen
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.6'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.16'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.16'
83
+ description: Format text for humans using a declarative, extensible API.
84
+ email: ahmad@instructure.com
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - lib/eskimo.rb
90
+ - lib/eskimo/component.rb
91
+ - lib/eskimo/components.rb
92
+ - lib/eskimo/renderer.rb
93
+ - lib/eskimo/version.rb
94
+ - spec/components_spec.rb
95
+ - spec/renderer_spec.rb
96
+ - spec/spec_helper.rb
97
+ homepage: https://github.com/amireh/eskimo
98
+ licenses:
99
+ - MIT
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 2.5.1
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.7.6
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: Declarative text formatting
121
+ test_files:
122
+ - spec/spec_helper.rb
123
+ - spec/renderer_spec.rb
124
+ - spec/components_spec.rb