elastic_tabstops 0.1.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: d1ff69b38a4101a8accf5a76413322ba1cbebeaad554a7c4ab6d29750b581005
4
+ data.tar.gz: a76ec437baf9959670a4998bc2dddd56283937a36c9074ab7e8c5ac27e8ba526
5
+ SHA512:
6
+ metadata.gz: 79a97edad288051b62d298dff9b2b09f701d027df43a33808a7b7384ef32f2331e2fdb591b4f0b9582e1bd1189795125698dc00bbfda55f3d78e623b7fbddb55
7
+ data.tar.gz: 48958bdb999a30fadf523a9bec98f87e2ea0ba51082be1fa248277411f8abe6f566cac48d4b35bee67b1a5ba9290f64f638360cedff68ccbcecbef5b355d5213
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.5.5
7
+ before_install: gem install bundler -v 2.0.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in elastic_tabstops.gemspec
4
+ gemspec
@@ -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
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>
@@ -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.
@@ -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).
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -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__)
@@ -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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -0,0 +1,3 @@
1
+ module ElasticTabstops
2
+ VERSION = "0.1.0"
3
+ 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: []