elastic_tabstops 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +38 -0
- align code.pdf +0 -0
- data/INSPIRATION/Elastic tabstops - a better way to indent and align code.webloc +8 -0
- data/INSPIRATION/src:text:tabwriter: - The Go Programming Language.webloc +8 -0
- data/INSPIRATION/tabwriter - The Go Programming Language.webloc +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +204 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/demo.rb +60 -0
- data/bin/setup +8 -0
- data/elastic_tabstops.gemspec +48 -0
- data/lib/elastic_tabstops.rb +17 -0
- data/lib/elastic_tabstops/formatter.rb +74 -0
- data/lib/elastic_tabstops/outstream_to_lines.rb +57 -0
- data/lib/elastic_tabstops/version.rb +3 -0
- metadata +181 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d1ff69b38a4101a8accf5a76413322ba1cbebeaad554a7c4ab6d29750b581005
|
4
|
+
data.tar.gz: a76ec437baf9959670a4998bc2dddd56283937a36c9074ab7e8c5ac27e8ba526
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 79a97edad288051b62d298dff9b2b09f701d027df43a33808a7b7384ef32f2331e2fdb591b4f0b9582e1bd1189795125698dc00bbfda55f3d78e623b7fbddb55
|
7
|
+
data.tar.gz: 48958bdb999a30fadf523a9bec98f87e2ea0ba51082be1fa248277411f8abe6f566cac48d4b35bee67b1a5ba9290f64f638360cedff68ccbcecbef5b355d5213
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
elastic_tabstops (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
byebug (11.0.1)
|
10
|
+
coderay (1.1.2)
|
11
|
+
ffaker (2.11.0)
|
12
|
+
method_source (0.9.2)
|
13
|
+
minitest (5.11.3)
|
14
|
+
minitest-rg (5.2.0)
|
15
|
+
minitest (~> 5.0)
|
16
|
+
pry (0.12.2)
|
17
|
+
coderay (~> 1.1.0)
|
18
|
+
method_source (~> 0.9.0)
|
19
|
+
pry-byebug (3.7.0)
|
20
|
+
byebug (~> 11.0)
|
21
|
+
pry (~> 0.10)
|
22
|
+
rake (10.5.0)
|
23
|
+
|
24
|
+
PLATFORMS
|
25
|
+
ruby
|
26
|
+
|
27
|
+
DEPENDENCIES
|
28
|
+
bundler (~> 2.0)
|
29
|
+
elastic_tabstops!
|
30
|
+
ffaker (~> 2.11.0)
|
31
|
+
minitest (~> 5.0)
|
32
|
+
minitest-rg (~> 5.0)
|
33
|
+
pry (~> 0.12.2)
|
34
|
+
pry-byebug (~> 3.7.0)
|
35
|
+
rake (~> 10.0)
|
36
|
+
|
37
|
+
BUNDLED WITH
|
38
|
+
2.0.1
|
align code.pdf
ADDED
Binary file
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
3
|
+
<plist version="1.0">
|
4
|
+
<dict>
|
5
|
+
<key>URL</key>
|
6
|
+
<string>http://nickgravgaard.com/elastic-tabstops/</string>
|
7
|
+
</dict>
|
8
|
+
</plist>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
3
|
+
<plist version="1.0">
|
4
|
+
<dict>
|
5
|
+
<key>URL</key>
|
6
|
+
<string>https://golang.org/src/text/tabwriter/</string>
|
7
|
+
</dict>
|
8
|
+
</plist>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
3
|
+
<plist version="1.0">
|
4
|
+
<dict>
|
5
|
+
<key>URL</key>
|
6
|
+
<string>https://golang.org/pkg/text/tabwriter/</string>
|
7
|
+
</dict>
|
8
|
+
</plist>
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 Thomas A. Boyer
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
# ElasticTabstops
|
2
|
+
This is a Ruby implementation of
|
3
|
+
[elastic tabstops](http://nickgravgaard.com/elastic-tabstops/).
|
4
|
+
|
5
|
+
Aligning output columns can greatly improve the legibility of program output.
|
6
|
+
Unfortunately, that can be difficult to arrange.
|
7
|
+
|
8
|
+
Elastic tabstops are an elegant solution to that problem. The idea is to
|
9
|
+
replace the classic "tabstops occur every 8 spaces" with the idea that
|
10
|
+
corresponding tabs on adjacent lines should define a column, and the tabstop
|
11
|
+
after that column's contents should be positioned to make that column just wide
|
12
|
+
enough for its data.
|
13
|
+
|
14
|
+
Tabs have always been elastic. This proposal makes the tab _stops_ elastic, as
|
15
|
+
well, by positioning them to fit the text.
|
16
|
+
|
17
|
+
As an example, this code writes several lines to an ElasticTabstops output
|
18
|
+
stream, where each line contains five tab-separated "words" of random length.
|
19
|
+
The elastic tabstops algorithm replaces tabs with spaces that neatly align the
|
20
|
+
columns.
|
21
|
+
```ruby
|
22
|
+
require "elastic_tabstops"
|
23
|
+
|
24
|
+
def word(letter); letter * rand(12) end;
|
25
|
+
def line(letters); letters.map { |letter| word(letter) }.join("\t") end;
|
26
|
+
|
27
|
+
et = ElasticTabstops::make_stream($stdout);
|
28
|
+
5.times do et.puts line(%w(a b c d e)) end; et.flush;
|
29
|
+
```
|
30
|
+
```text
|
31
|
+
aaaaaa bbbbbbbbbbb c eeeeeeeee
|
32
|
+
aaaaaaaaaa bbbbbbbbbb cccc ddddddddd eeeeeeee
|
33
|
+
aaaaaaaa bb cc ddddddd eeeee
|
34
|
+
aaaaa bbbbbb ccccc dddddddddd ee
|
35
|
+
aaaa bbb ccccc ddddd eeeeeeee
|
36
|
+
```
|
37
|
+
|
38
|
+
## Details
|
39
|
+
|
40
|
+
Data to be formatted as part of a column must be terminated with a tab character
|
41
|
+
(`"\t"`). Any data that doesn't have a terminating tab (that is, from the last
|
42
|
+
tab to the end of the line) is not part of a column, and will be written
|
43
|
+
(without padding) following the last column.
|
44
|
+
|
45
|
+
A column is terminated by a line that doesn't contain that column. Having no
|
46
|
+
content in a column is okay, but if you want the column to continue
|
47
|
+
past that line, it must contain at least the tab terminator for that column.
|
48
|
+
|
49
|
+
This is easier to see with an example.
|
50
|
+
```
|
51
|
+
elastic_tabstops_stream = ElasticTabstops::make_stream($stdout);
|
52
|
+
[ "line 1: A|B|C",
|
53
|
+
"line 2: a|b|c|",
|
54
|
+
"line 3: aaa||cc|",
|
55
|
+
"line 4: a|bbbbbbbb|c|",
|
56
|
+
"line 5: A|",
|
57
|
+
"line 6: A|X|Y|Z|",
|
58
|
+
"line 7: a||yyyyyyyyyy|zzzz|",
|
59
|
+
"line 8: a|x|yyy|z|",
|
60
|
+
"line 9: aaaaaaaaaaaa|xx|yyyyy|z|",
|
61
|
+
].each { |s| elastic_tabstops_stream.puts s.gsub("|", "\t") }
|
62
|
+
elastic_tabstops_stream.flush # required (to process any buffered data)
|
63
|
+
```
|
64
|
+
(The `gsub` allows the example to use '|' as a visible stand-in for the required
|
65
|
+
tab characters.)
|
66
|
+
|
67
|
+
Here's the output:
|
68
|
+
```
|
69
|
+
line 1: A B C
|
70
|
+
line 2: a b c
|
71
|
+
line 3: aaa cc
|
72
|
+
line 4: a bbbbbbbb c
|
73
|
+
line 5: A
|
74
|
+
line 6: A X Y Z
|
75
|
+
line 7: a yyyyyyyyyy zzzz
|
76
|
+
line 8: a x yyy z
|
77
|
+
line 9: aaaaaaaaaaaa xx yyyyy z
|
78
|
+
```
|
79
|
+
|
80
|
+
The first four lines have columns A, B, and C, but then line 5 has only
|
81
|
+
column A. That terminates columns B and C. Note that column B of line 3 is
|
82
|
+
empty, but column B continues to line 4 because the terminating tab was present.
|
83
|
+
|
84
|
+
Lines 6 through 9 have columns A, X, Y, and Z. Columns X and Y are unconnected
|
85
|
+
to columns B and C, because B and C were terminated at line 5.
|
86
|
+
|
87
|
+
Column A is never broken, so it spans the entire output.
|
88
|
+
|
89
|
+
## Installation
|
90
|
+
|
91
|
+
Add this line to your application's Gemfile:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
gem 'elastic_tabstops'
|
95
|
+
```
|
96
|
+
|
97
|
+
And then execute:
|
98
|
+
|
99
|
+
$ bundle
|
100
|
+
|
101
|
+
Or install it yourself as:
|
102
|
+
|
103
|
+
$ gem install elastic_tabstops
|
104
|
+
|
105
|
+
## Usage
|
106
|
+
|
107
|
+
First, you create an ElasticTabstop output stream. The `make_stream` takes a
|
108
|
+
single argument, which is the output stream you want to receive the formatted
|
109
|
+
results.
|
110
|
+
```ruby
|
111
|
+
stream = ElasticTabstops::make_stream($stdout);
|
112
|
+
```
|
113
|
+
|
114
|
+
Then, write your data to the stream, terminating each column's data
|
115
|
+
with a tab. Any data (at the end of line) that doesn't end in a tab is not
|
116
|
+
part of a column, and will be written following the last column.
|
117
|
+
|
118
|
+
ElasticTabstops streams provide `puts`, `print`, and `write` methods.
|
119
|
+
|
120
|
+
When you're done writing to the stream, you need to call its `flush` method.
|
121
|
+
The algorithm has to buffer the output data, because it can't generate
|
122
|
+
output until it knows how wide the columns are, and it can't know for sure how
|
123
|
+
wide the columns are until all the column data has been seen.
|
124
|
+
The `flush` tells the stream to close off all open columns and emit the output.
|
125
|
+
|
126
|
+
Generating an output line with no tab characters will also flush the buffer,
|
127
|
+
because it terminates all open columns.
|
128
|
+
|
129
|
+
Here's a more extensive demo. It prints an array of hashes as a table.
|
130
|
+
```ruby
|
131
|
+
require "ffaker"
|
132
|
+
require "bundler/setup"
|
133
|
+
require "elastic_tabstops"
|
134
|
+
|
135
|
+
def word
|
136
|
+
FFaker::Lorem.word
|
137
|
+
end
|
138
|
+
|
139
|
+
def phrase(min_words=0, max_words=3)
|
140
|
+
rand(min_words..max_words).times.map { word }.join(' ')
|
141
|
+
end
|
142
|
+
|
143
|
+
# Generate some sample data, in the form of hashes containing random text
|
144
|
+
keys = 5.times.map { phrase(1, 1) }
|
145
|
+
#=> ["iusto", "natus", "temporibus", "nobis", "sit"]
|
146
|
+
data = 10.times.map do
|
147
|
+
values = keys.size.times.map { phrase(0,2) }
|
148
|
+
Hash[keys.zip(values)]
|
149
|
+
end
|
150
|
+
#=> [{"iusto"=>"", "natus"=>"", "temporibus"=>"", "nobis"=>"sit", "sit"=>"explicabo blanditiis"},
|
151
|
+
# {"iusto"=>"et", "natus"=>"nulla voluptas", "temporibus"=>"assumenda", "nobis"=>"omnis non", "sit"=>"autem"},
|
152
|
+
# {"iusto"=>"repellendus distinctio", "natus"=>"consequuntur ipsum", "temporibus"=>"ut", "nobis"=>"", "sit"=>"sit"},
|
153
|
+
# {"iusto"=>"cum", "natus"=>"", "temporibus"=>"", "nobis"=>"qui ipsa", "sit"=>"soluta ut"},
|
154
|
+
# {"iusto"=>"", "natus"=>"nam ex", "temporibus"=>"delectus aut", "nobis"=>"", "sit"=>""},
|
155
|
+
# {"iusto"=>"qui molestiae", "natus"=>"", "temporibus"=>"ut eos", "nobis"=>"qui vel", "sit"=>""},
|
156
|
+
# {"iusto"=>"et nesciunt", "natus"=>"provident", "temporibus"=>"veniam autem", "nobis"=>"", "sit"=>""},
|
157
|
+
# {"iusto"=>"", "natus"=>"", "temporibus"=>"sint accusamus", "nobis"=>"cumque voluptates", "sit"=>"voluptas"},
|
158
|
+
# {"iusto"=>"est", "natus"=>"quidem", "temporibus"=>"rerum non", "nobis"=>"illo cupiditate", "sit"=>""},
|
159
|
+
# {"iusto"=>"", "natus"=>"odio ratione", "temporibus"=>"porro", "nobis"=>"et numquam", "sit"=>""}]
|
160
|
+
|
161
|
+
# Write the array of hashes in columns.
|
162
|
+
# This method assumes that the first hash contains all the keys that matter.
|
163
|
+
def write_table(hashes)
|
164
|
+
remember_original_stdout = $stdout
|
165
|
+
$stdout = ElasticTabstops::make_stream($stdout);
|
166
|
+
keys = hashes.first.keys
|
167
|
+
puts keys.join("\t")
|
168
|
+
puts keys.map { |k| '=' * k.size }.join("\t")
|
169
|
+
hashes.each do |h|
|
170
|
+
puts h.fetch_values(*keys).join("\t")
|
171
|
+
end
|
172
|
+
$stdout.flush
|
173
|
+
$stdout = remember_original_stdout
|
174
|
+
end
|
175
|
+
|
176
|
+
write_table(data);
|
177
|
+
# iusto natus temporibus nobis sit
|
178
|
+
# ===== ===== ========== ===== ===
|
179
|
+
# sit explicabo blanditiis
|
180
|
+
# et nulla voluptas assumenda omnis non autem
|
181
|
+
# repellendus distinctio consequuntur ipsum ut sit
|
182
|
+
# cum qui ipsa soluta ut
|
183
|
+
# nam ex delectus aut
|
184
|
+
# qui molestiae ut eos qui vel
|
185
|
+
# et nesciunt provident veniam autem
|
186
|
+
# sint accusamus cumque voluptates voluptas
|
187
|
+
# est quidem rerum non illo cupiditate
|
188
|
+
# odio ratione porro et numquam
|
189
|
+
```
|
190
|
+
|
191
|
+
## Development
|
192
|
+
|
193
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
194
|
+
Then, run `rake test` to run the tests.
|
195
|
+
|
196
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
197
|
+
|
198
|
+
## Contributing
|
199
|
+
|
200
|
+
Bug reports and pull requests are welcome on GitHub at `https://github.com/perlmonger42/elastic_tabstops`.
|
201
|
+
|
202
|
+
## License
|
203
|
+
|
204
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "elastic_tabstops"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/demo.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "ffaker"
|
3
|
+
require "bundler/setup"
|
4
|
+
require "elastic_tabstops"
|
5
|
+
|
6
|
+
def word
|
7
|
+
FFaker::Lorem.word
|
8
|
+
end
|
9
|
+
|
10
|
+
def phrase(min_words=0, max_words=3)
|
11
|
+
rand(min_words..max_words).times.map { word }.join(' ')
|
12
|
+
end
|
13
|
+
|
14
|
+
# Generate some sample data, in the form of hashes containing random text
|
15
|
+
keys = 5.times.map { phrase(1, 1) }
|
16
|
+
#=> ["iusto", "natus", "temporibus", "nobis", "sit"]
|
17
|
+
|
18
|
+
data = 10.times.map do
|
19
|
+
values = keys.size.times.map { phrase(0,2) }
|
20
|
+
Hash[keys.zip(values)]
|
21
|
+
end
|
22
|
+
#=> [{"iusto"=>"", "natus"=>"", "temporibus"=>"", "nobis"=>"sit", "sit"=>"explicabo blanditiis"},
|
23
|
+
# {"iusto"=>"et", "natus"=>"nulla voluptas", "temporibus"=>"assumenda", "nobis"=>"omnis non", "sit"=>"autem"},
|
24
|
+
# {"iusto"=>"repellendus distinctio", "natus"=>"consequuntur ipsum", "temporibus"=>"ut", "nobis"=>"", "sit"=>"sit"},
|
25
|
+
# {"iusto"=>"cum", "natus"=>"", "temporibus"=>"", "nobis"=>"qui ipsa", "sit"=>"soluta ut"},
|
26
|
+
# {"iusto"=>"", "natus"=>"nam ex", "temporibus"=>"delectus aut", "nobis"=>"", "sit"=>""},
|
27
|
+
# {"iusto"=>"qui molestiae", "natus"=>"", "temporibus"=>"ut eos", "nobis"=>"qui vel", "sit"=>""},
|
28
|
+
# {"iusto"=>"et nesciunt", "natus"=>"provident", "temporibus"=>"veniam autem", "nobis"=>"", "sit"=>""},
|
29
|
+
# {"iusto"=>"", "natus"=>"", "temporibus"=>"sint accusamus", "nobis"=>"cumque voluptates", "sit"=>"voluptas"},
|
30
|
+
# {"iusto"=>"est", "natus"=>"quidem", "temporibus"=>"rerum non", "nobis"=>"illo cupiditate", "sit"=>""},
|
31
|
+
# {"iusto"=>"", "natus"=>"odio ratione", "temporibus"=>"porro", "nobis"=>"et numquam", "sit"=>""}]
|
32
|
+
|
33
|
+
# Write the array of hashes in colums.
|
34
|
+
# This method assumes that the first hash contains all the keys that matter.
|
35
|
+
def write_table(hashes)
|
36
|
+
remember_original_stdout = $stdout
|
37
|
+
$stdout = ElasticTabstops::make_stream($stdout);
|
38
|
+
keys = hashes.first.keys
|
39
|
+
puts keys.join("\t")
|
40
|
+
puts keys.map { |k| '=' * k.size }.join("\t")
|
41
|
+
hashes.each do |h|
|
42
|
+
puts h.fetch_values(*keys).join("\t")
|
43
|
+
end
|
44
|
+
$stdout.flush
|
45
|
+
$stdout = remember_original_stdout
|
46
|
+
end
|
47
|
+
|
48
|
+
write_table(data);
|
49
|
+
# iusto natus temporibus nobis sit
|
50
|
+
# ===== ===== ========== ===== ===
|
51
|
+
# sit explicabo blanditiis
|
52
|
+
# et nulla voluptas assumenda omnis non autem
|
53
|
+
# repellendus distinctio consequuntur ipsum ut sit
|
54
|
+
# cum qui ipsa soluta ut
|
55
|
+
# nam ex delectus aut
|
56
|
+
# qui molestiae ut eos qui vel
|
57
|
+
# et nesciunt provident veniam autem
|
58
|
+
# sint accusamus cumque voluptates voluptas
|
59
|
+
# est quidem rerum non illo cupiditate
|
60
|
+
# odio ratione porro et numquam
|
data/bin/setup
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "elastic_tabstops/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "elastic_tabstops"
|
8
|
+
spec.version = ElasticTabstops::VERSION
|
9
|
+
spec.authors = ["Thomas A. Boyer"]
|
10
|
+
spec.email = ["thom at boyers.org"]
|
11
|
+
|
12
|
+
spec.summary = %q{An output stream that makes columnar output easy.}
|
13
|
+
spec.description = %q{This is an implementation of [elastic tabstops](http://nickgravgaard.com/elastic-tabstops/) in an output-stream filter.
|
14
|
+
|
15
|
+
Data written to an elastic tabstop output stream is reformatted to align
|
16
|
+
columns. Columns are made up of tab-terminated cells in adjacent lines of output.
|
17
|
+
}
|
18
|
+
spec.homepage = "https://github.com/perlmonger42/elastic_tabstops"
|
19
|
+
spec.license = "MIT"
|
20
|
+
|
21
|
+
if spec.respond_to?(:metadata)
|
22
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
23
|
+
|
24
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
25
|
+
spec.metadata["source_code_uri"] = "https://github.com/perlmonger42/elastic_tabstops"
|
26
|
+
spec.metadata["changelog_uri"] = "https://github.com/perlmonger42/elastic_tabstops/blob/master/CHANGELOG.md"
|
27
|
+
else
|
28
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
29
|
+
"public gem pushes."
|
30
|
+
end
|
31
|
+
|
32
|
+
# Specify which files should be added to the gem when it is released.
|
33
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
34
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
35
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
36
|
+
end
|
37
|
+
spec.bindir = "exe"
|
38
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
39
|
+
spec.require_paths = ["lib"]
|
40
|
+
|
41
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
42
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
43
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
44
|
+
spec.add_development_dependency "minitest-rg", "~> 5.0"
|
45
|
+
spec.add_development_dependency "pry", "~> 0.12.2"
|
46
|
+
spec.add_development_dependency 'pry-byebug', '~> 3.7', '>= 3.7.0'
|
47
|
+
spec.add_development_dependency "ffaker", "~> 2.11", ">= 2.11.0"
|
48
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "elastic_tabstops/version"
|
2
|
+
require "elastic_tabstops/formatter"
|
3
|
+
require "elastic_tabstops/outstream_to_lines"
|
4
|
+
require 'English'
|
5
|
+
|
6
|
+
module ElasticTabstops
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
def self.make_stream(output_stream, tabchar: "\t", padchar: ' ')
|
10
|
+
formatter = ElasticTabstops::Formatter.new(
|
11
|
+
output_stream,
|
12
|
+
tabchar: tabchar,
|
13
|
+
padchar: padchar
|
14
|
+
)
|
15
|
+
ElasticTabstops::OutstreamToLines.new(formatter)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module ElasticTabstops
|
2
|
+
class Formatter
|
3
|
+
|
4
|
+
def initialize(output_stream, tabchar: "\t", padchar: ' ')
|
5
|
+
@outstream = output_stream
|
6
|
+
@lines = []
|
7
|
+
@ctrls = []
|
8
|
+
@tabchar = tabchar
|
9
|
+
@padchar = padchar
|
10
|
+
@split_re = /(?<=#{Regexp.escape(tabchar)})/
|
11
|
+
end
|
12
|
+
|
13
|
+
# PRECONDITION: text == '' || /\A.*\r?\n\z/ === text
|
14
|
+
# In the regex above, `\A` matches beginning-of-string, and `\z` matches
|
15
|
+
# end-of-string. The absence of a multiline modifier (as in /abc/m) means
|
16
|
+
# `.` does NOT match newline.
|
17
|
+
#
|
18
|
+
# So, in English: `text` is a string, and it is either the empty string, or
|
19
|
+
# it is a sequence of non-newline characters terminated by a newline.
|
20
|
+
def line(text)
|
21
|
+
texts = text.split(@split_re)
|
22
|
+
|
23
|
+
# Ensure that a non-tab-terminated text exists at the end of texts
|
24
|
+
texts << '' if texts.empty? || texts[-1][-1] == @tabchar
|
25
|
+
|
26
|
+
emit_line_of_cells(texts)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# PRECONDITION: texts === Array && texts.size > 0 && texts[-1][-1] != @tabchar
|
32
|
+
def emit_line_of_cells(texts)
|
33
|
+
# Remove the non-tab-terminated cell from the end.
|
34
|
+
last = texts.pop
|
35
|
+
|
36
|
+
# Make sure `@ctrls` covers all columns in this input line.
|
37
|
+
@ctrls.push({ width: 0 }) while @ctrls.size < texts.size
|
38
|
+
|
39
|
+
# Convert the array of strings into an array of cell hashes
|
40
|
+
cells = texts.map.with_index do |text, i|
|
41
|
+
text = text[0..-2] # remove trailing tab
|
42
|
+
ctrl = @ctrls[i]
|
43
|
+
ctrl[:width] = text.size if text.size > ctrl[:width]
|
44
|
+
{ text: text, ctrl: ctrl }
|
45
|
+
end
|
46
|
+
|
47
|
+
# Remove ctrl cells for columns that don't appear in this line.
|
48
|
+
# Such columns had data in the previous output line, but not this one.
|
49
|
+
@ctrls.pop while @ctrls.size > cells.size
|
50
|
+
|
51
|
+
# Restore the special `last` cell, and store the cells in @lines.
|
52
|
+
cells << { text: last } # NOTE: no :ctrl field
|
53
|
+
@lines.push(cells)
|
54
|
+
|
55
|
+
# If cells.size == 1, it's only the special `last` cell; there are no
|
56
|
+
# formattable columns in this line. That means all the pending output
|
57
|
+
# columns have terminated, and we finally know the :width's are final.
|
58
|
+
write_formatted_text_to_output if cells.size == 1
|
59
|
+
end
|
60
|
+
|
61
|
+
def write_formatted_text_to_output
|
62
|
+
@lines.each do |line|
|
63
|
+
line.each do |cell|
|
64
|
+
s = cell[:text]
|
65
|
+
@outstream.write(s)
|
66
|
+
if cell.key?(:ctrl)
|
67
|
+
@outstream.write(@padchar * (cell[:ctrl][:width] + 1 - s.size))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
@lines = []
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module ElasticTabstops
|
2
|
+
class OutstreamToLines
|
3
|
+
|
4
|
+
# INVARIANT: @text contains no newlines
|
5
|
+
|
6
|
+
def initialize(liner)
|
7
|
+
@text = ''
|
8
|
+
@liner = liner
|
9
|
+
end
|
10
|
+
|
11
|
+
def puts(*args)
|
12
|
+
sio = StringIO.new
|
13
|
+
result = sio.puts(*args)
|
14
|
+
add_text(sio.string)
|
15
|
+
result
|
16
|
+
end
|
17
|
+
|
18
|
+
def print(*args)
|
19
|
+
sio = StringIO.new
|
20
|
+
result = sio.print(*args)
|
21
|
+
add_text(sio.string)
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
25
|
+
def write(*args)
|
26
|
+
sio = StringIO.new
|
27
|
+
result = sio.write(*args)
|
28
|
+
add_text(sio.string)
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
def flush
|
33
|
+
@liner.line(@text) unless @text.empty?
|
34
|
+
@liner.line('') # signals end-of-input to the @liner
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Add text to @text.
|
40
|
+
# Move completed lines (at the beginning of @text) to @liner.
|
41
|
+
def add_text(text, flush: false)
|
42
|
+
# ASSERTION: @text contains no newlines, due to class invariant
|
43
|
+
@text += text
|
44
|
+
|
45
|
+
# At this point, @text _may_ contain newlines, thus violating the class
|
46
|
+
# invariant. Move any full lines _out_ of @text and into @liner.
|
47
|
+
pos = 0
|
48
|
+
while (m = @text.match(/\G.*?\r?\n/, pos)) do
|
49
|
+
@liner.line m[0]
|
50
|
+
pos += m[0].size
|
51
|
+
end
|
52
|
+
@text = @text[pos..-1]
|
53
|
+
|
54
|
+
# ASSERTION: @text contains no newlines; invariant has been restored
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
metadata
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: elastic_tabstops
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Thomas A. Boyer
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-08-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest-rg
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.12.2
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.12.2
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry-byebug
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.7'
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 3.7.0
|
93
|
+
type: :development
|
94
|
+
prerelease: false
|
95
|
+
version_requirements: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - "~>"
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '3.7'
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 3.7.0
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: ffaker
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '2.11'
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: 2.11.0
|
113
|
+
type: :development
|
114
|
+
prerelease: false
|
115
|
+
version_requirements: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - "~>"
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '2.11'
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: 2.11.0
|
123
|
+
description: |
|
124
|
+
This is an implementation of [elastic tabstops](http://nickgravgaard.com/elastic-tabstops/) in an output-stream filter.
|
125
|
+
|
126
|
+
Data written to an elastic tabstop output stream is reformatted to align
|
127
|
+
columns. Columns are made up of tab-terminated cells in adjacent lines of output.
|
128
|
+
email:
|
129
|
+
- thom at boyers.org
|
130
|
+
executables: []
|
131
|
+
extensions: []
|
132
|
+
extra_rdoc_files: []
|
133
|
+
files:
|
134
|
+
- ".gitignore"
|
135
|
+
- ".travis.yml"
|
136
|
+
- Gemfile
|
137
|
+
- Gemfile.lock
|
138
|
+
- INSPIRATION/Elastic tabstops - a better way to indent and align code.pdf
|
139
|
+
- INSPIRATION/Elastic tabstops - a better way to indent and align code.webloc
|
140
|
+
- 'INSPIRATION/src:text:tabwriter: - The Go Programming Language.webloc'
|
141
|
+
- INSPIRATION/tabwriter - The Go Programming Language.webloc
|
142
|
+
- LICENSE.txt
|
143
|
+
- README.md
|
144
|
+
- Rakefile
|
145
|
+
- bin/console
|
146
|
+
- bin/demo.rb
|
147
|
+
- bin/setup
|
148
|
+
- elastic_tabstops.gemspec
|
149
|
+
- lib/elastic_tabstops.rb
|
150
|
+
- lib/elastic_tabstops/formatter.rb
|
151
|
+
- lib/elastic_tabstops/outstream_to_lines.rb
|
152
|
+
- lib/elastic_tabstops/version.rb
|
153
|
+
homepage: https://github.com/perlmonger42/elastic_tabstops
|
154
|
+
licenses:
|
155
|
+
- MIT
|
156
|
+
metadata:
|
157
|
+
allowed_push_host: https://rubygems.org
|
158
|
+
homepage_uri: https://github.com/perlmonger42/elastic_tabstops
|
159
|
+
source_code_uri: https://github.com/perlmonger42/elastic_tabstops
|
160
|
+
changelog_uri: https://github.com/perlmonger42/elastic_tabstops/blob/master/CHANGELOG.md
|
161
|
+
post_install_message:
|
162
|
+
rdoc_options: []
|
163
|
+
require_paths:
|
164
|
+
- lib
|
165
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
166
|
+
requirements:
|
167
|
+
- - ">="
|
168
|
+
- !ruby/object:Gem::Version
|
169
|
+
version: '0'
|
170
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
171
|
+
requirements:
|
172
|
+
- - ">="
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '0'
|
175
|
+
requirements: []
|
176
|
+
rubyforge_project:
|
177
|
+
rubygems_version: 2.7.6
|
178
|
+
signing_key:
|
179
|
+
specification_version: 4
|
180
|
+
summary: An output stream that makes columnar output easy.
|
181
|
+
test_files: []
|