eskimo 1.0.0

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