elastic_tabstops 0.1.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.
- 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: []
|