columnist 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 35ce2268e5eb282ea238c82f068afe57dc8140e4
4
+ data.tar.gz: cf74dcb33851da6ae27429675ce80d06288f0dc0
5
+ SHA512:
6
+ metadata.gz: a76d6cb4ee88847ed5dd3ddc465d56cb3fc6f7f886a1e4c16dc80ce787f3ec49d52e6a81ec258250d9c39bce28cab12f31073d3d328e38f478984eecd90082b5
7
+ data.tar.gz: e5b603a886e092c61b43472a984027e275b81195db5eff1894b533c01a32a9b17c1ba60bde9f20b2f40079c03eac9e0136c3b9b2c56946f2819a70609db89742
data/README.md ADDED
@@ -0,0 +1,150 @@
1
+ ## Columnist
2
+
3
+ This gem provides a DSL that makes it easy to write reports of various types in ruby. It eliminates
4
+ the need to litter your source with *puts* statements, instead providing a more readable, expressive
5
+ interface to your application. Some of the best features include:
6
+
7
+ * Formatters that automatically indicate progress
8
+ * Table syntax similar to HTML that makes it trivial to format your data in rows and columns
9
+ * Easily created headers and footers for your report
10
+ * Output suppression that makes it easy for your script to support a _quiet_ flag
11
+ * Capture report output as a string
12
+
13
+ The latest release, thanks to a contribution from [Josh Brown](https://github.com/tobijb), allows you
14
+ to choose between UTF8 or ASCII for drawing tables. By default it will use UTF8 if your system
15
+ supports it. Here is an example of output you can generate easily with "the reporter":
16
+
17
+ ![Screenshot](http://i.imgur.com/5izCf.png)
18
+
19
+ ### Installation
20
+
21
+ It is up on rubygems.org so add it to your bundle in the Gemfile
22
+
23
+ ```bash
24
+ gem 'columnist', '>=1.0'
25
+ ```
26
+
27
+ or do it the old fashioned way:
28
+
29
+ ```bash
30
+ gem install columnist
31
+ ```
32
+
33
+ ### Usage
34
+
35
+ The gem provides a mixin that can be included in your scripts.
36
+
37
+ ```ruby
38
+ require 'columnist'
39
+
40
+ class MyReport
41
+ include Columnist
42
+ ...
43
+ end
44
+ ```
45
+
46
+ ### [Wiki](https://github.com/alb3rtuk/columnist)
47
+
48
+ The [Wiki](https://github.com/alb3rtuk/columnist) has all of the documentation
49
+ necessary for getting you started.
50
+
51
+ ### API Reference
52
+
53
+ There are several methods the mixin provides that do not depend on the formatter used:
54
+
55
+ * _header(hash)_ and _footer(hash)_
56
+ * _:title_ - The title text for the section. _Default: 'Report'_
57
+ * _:width_ - The width in characters for the section. _Default: 100_
58
+ * _:align_ - 'left'|'right'|'center' align the title text. _Default: 'left'_
59
+ * _:spacing_ - Number of vertical lines to leave as spacing after|before the header|footer.
60
+ _Default: 1_
61
+ * _:timestamp_ - Include a line indicating the timestamp below|above the header|footer text.
62
+ Either true|false. _Default: false_
63
+ * _:rule_ - true|false indicates whether to include a horizontal rule below|above the
64
+ header|footer. _Default: false_
65
+ * _:color_ - The color to use for the terminal output i.e. 'red' or 'blue' or 'green'
66
+ * _:bold_ - true|false to boldface the font
67
+ * _report(hash) {block}_
68
+ * The first argument is a hash that defines the options for the method. See the details in the
69
+ formatter section for allowed values.
70
+ * The second argument is a block of ruby code that you want executed within the context of the
71
+ reporter. Any ruby code is allowed. See the examples that follow in the formatter sections for
72
+ details.
73
+ * _formatter=(string)_
74
+ * Factory method indicating the formatter you want your application to use. At present the 2
75
+ formatters are (_Default: 'nested'_):
76
+ * 'progress' - Use the progress formatter
77
+ * 'nested' - Use the nested (or documentation) formatter
78
+ * _horizontal_rule(hash)_
79
+ * _:char_ - The character used to build the rule. _Default: '-'_
80
+ * _:width_ - The width in characters of the rule. _Default: 100_
81
+ * _:color_ - The color to use for the terminal output i.e. 'red' or 'blue' or 'green'
82
+ * _:bold_ - true|false to boldface the font
83
+ * _vertical_spacing(int)_
84
+ * Number of blank lines to output. _Default: 1_
85
+ * _datetime(hash)_
86
+ * _:align_ - 'left'|'center'|'right' alignment of the timestamp. _Default: 'left'_
87
+ * _:width_ - The width of the string in characters. _Default: 100_
88
+ * _:format_ - Any allowed format from #strftime#. _Default: %Y-%m-%d %H:%I:%S%p_
89
+ * _:color_ - The color to use for the terminal output i.e. 'red' or 'blue' or 'green'
90
+ * _:bold_ - true|false to boldface the font
91
+ * _aligned(string, hash)_
92
+ * _text_ - String to display
93
+ * _:align_ - 'left'|'right'|'center' align the string text. _Default: 'left'_
94
+ * _:width_ - The width in characters of the string text. _Default: 100_
95
+ * _:color_ - The color to use for the terminal output i.e. 'red' or 'blue' or 'green'
96
+ * _:bold_ - true|false to boldface the font
97
+ * _table(hash) {block}_
98
+ * The first argument is a hash that defines properties of the table.
99
+ * _:border_ - true|false indicates whether to include borders around the table cells
100
+ * _:encoding_ - :ascii or :unicode (default unicode)
101
+ * The second argument is a block which includes calls the to the _row_ method
102
+ * _row {block}_
103
+ * _:header_ - Set to true to indicate if this is a header row in the table.
104
+ * _:color_ - The color to use for the terminal output i.e. 'red' or 'blue' or 'green'
105
+ * _:bold_ - true|false to boldface the font
106
+ * _column(string, hash)_
107
+ * _text_ - String to display in the table cell
108
+ * _options_ - The options to define the column
109
+ * :width - defines the width of the column
110
+ * :padding - The number of spaces to put on both the left and right of the text.
111
+ * :align - Allowed values are left|right|center
112
+ * :color - The color to use for the terminal output i.e. 'red' or 'blue' or 'green'
113
+ * :bold - true|false to boldface the font
114
+ * _suppress_output_ - Suppresses output stream that goes to STDOUT
115
+ * _capture_output_ - Captures all of the output stream to a string and restores output to STDOUT
116
+ * _restore_output_ - Restores the output stream to STDOUT
117
+
118
+ ### To Do
119
+
120
+ * Add a formatter that supports html output
121
+ * Add the ability for a column to span across others in a table
122
+
123
+ ### Contributors
124
+
125
+ * [Josh Brown](https://github.com/tobijb) added the ability to encode tables in either ascii or utf8
126
+ * [Stefan Frank](https://github.com/mugwump) for raising the issue that he could not capture report
127
+ output in a variable as a string
128
+ * [Mike Gunderloy](https://github.com/ffmike) for suggesting the need for suppressing output and
129
+ putting together a fantastic pull request and discussion
130
+ * [Jason Rogers](https://github.com/jacaetevha) and [Peter Suschlik](https://github.com/splattael)
131
+ for their contributions as well on items I missed
132
+
133
+ ### License
134
+
135
+ Copyright (c) 2011-2014 Albert Rannetsperger
136
+
137
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
138
+ associated documentation files (the "Software"), to deal in the Software without restriction,
139
+ including without limitation the rights to use, copy, modify, merge, publish, distribute,
140
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
141
+ furnished to do so, subject to the following conditions:
142
+
143
+ The above copyright notice and this permission notice shall be included in all copies or substantial
144
+ portions of the Software.
145
+
146
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
147
+ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
148
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
149
+ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
150
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,99 @@
1
+ require 'colored'
2
+
3
+ module Columnist
4
+ class Column
5
+ include OptionsValidator
6
+
7
+ VALID_OPTIONS = [:width, :padding, :align, :color, :bold, :underline, :reversed]
8
+ attr_accessor :text, :size, *VALID_OPTIONS
9
+
10
+ def initialize(text = nil, options = {})
11
+ self.validate_options(options, *VALID_OPTIONS)
12
+ self.text = text.to_s
13
+ self.width = options[:width] || 10
14
+ self.align = options[:align] || 'left'
15
+ self.padding = options[:padding] || 0
16
+ self.color = options[:color] || nil
17
+ self.bold = options[:bold] || false
18
+ self.underline = options[:underline] || false
19
+ self.reversed = options[:reversed] || false
20
+
21
+ raise ArgumentError unless self.width > 0
22
+ raise ArgumentError unless self.padding.to_s.match(/^\d+$/)
23
+ end
24
+
25
+ def size
26
+ self.width - 2 * self.padding
27
+ end
28
+
29
+ def required_width
30
+ self.text.to_s.size + 2 * self.padding
31
+ end
32
+
33
+ def screen_rows
34
+ if self.text.nil? || self.text.empty?
35
+ [' ' * self.width]
36
+ else
37
+ self.text.scan(/.{1,#{self.size}}/m).map { |s| to_cell(s) }
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def to_cell(str)
44
+ # NOTE: For making underline and reversed work Change so that based on the
45
+ # unformatted text it determines how much spacing to add left and right
46
+ # then colorize the cell text
47
+ cell = str.empty? ? blank_cell : aligned_cell(str)
48
+ padding_str = ' ' * self.padding
49
+ padding_str + colorize(cell) + padding_str
50
+ end
51
+
52
+ def blank_cell
53
+ ' ' * self.size
54
+ end
55
+
56
+ def aligned_cell(str)
57
+ case self.align
58
+ when 'left'
59
+ str.ljust(self.size)
60
+ when 'right'
61
+ str.rjust(self.size)
62
+ when 'center'
63
+ str.ljust((self.size - str.size)/2.0 + str.size).rjust(self.size)
64
+ end
65
+ end
66
+
67
+ def colorize(str)
68
+ str = str.send('bold') if self.bold
69
+ case self.color
70
+ when 'red'
71
+ return "\x1B[38;5;9m#{str}\x1B[38;5;256m"
72
+ when 'green'
73
+ return "\x1B[38;5;10m#{str}\x1B[38;5;256m"
74
+ when 'yellow'
75
+ return "\x1B[38;5;11m#{str}\x1B[38;5;256m"
76
+ when 'blue'
77
+ return "\x1B[38;5;33m#{str}\x1B[38;5;256m"
78
+ when 'magenta'
79
+ return "\x1B[38;5;13m#{str}\x1B[38;5;256m"
80
+ when 'cyan'
81
+ return "\x1B[38;5;14m#{str}\x1B[38;5;256m"
82
+ when 'gray'
83
+ return "\x1B[38;5;240m#{str}\x1B[38;5;256m"
84
+ when 'white'
85
+ return "\x1B[38;5;255m#{str}\x1B[38;5;256m"
86
+ when 'black'
87
+ return "\x1B[38;5;0m#{str}\x1B[38;5;256m"
88
+ end
89
+ if is_number?(self.color)
90
+ str = "\x1B[38;5;#{self.color}m#{str}\x1B[38;5;256m"
91
+ end
92
+ str
93
+ end
94
+
95
+ def is_number?(str)
96
+ true if Integer(str) rescue false
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,70 @@
1
+ require 'singleton'
2
+ require 'colored'
3
+
4
+ module Columnist
5
+ class NestedFormatter
6
+ include Singleton
7
+ include OptionsValidator
8
+
9
+ VALID_OPTIONS = [:message, :type, :complete, :indent_size, :color, :bold]
10
+ attr_accessor :indent_size, :complete_string, :message_string, :color, :bold
11
+
12
+ def format(options, block)
13
+ self.validate_options(options, *VALID_OPTIONS)
14
+
15
+ indent_level :incr
16
+
17
+ padding = ' ' * @indent_level * (options[:indent_size] || self.indent_size)
18
+
19
+ message_str = padding + (options[:message] || self.message_string)
20
+ complete_str = options[:complete] || self.complete_string
21
+
22
+ if options[:type] == 'inline'
23
+ colorize("#{message_str}...", true, options)
24
+ else
25
+ colorize(message_str, false, options)
26
+ complete_str = padding + complete_str
27
+ end
28
+
29
+ block.call
30
+
31
+ colorize(complete_str, false, options)
32
+
33
+ indent_level :decr
34
+ end
35
+
36
+ def message_string
37
+ @message_string ||= 'working'
38
+ end
39
+
40
+ def complete_string
41
+ @complete_string ||= 'complete'
42
+ end
43
+
44
+ def indent_size
45
+ @indent_size ||= 2
46
+ end
47
+
48
+ private
49
+
50
+ def colorize(str, inline, options)
51
+ str = str.send(options[:color]) if options[:color]
52
+ str = str.bold if options[:bold]
53
+
54
+ if inline
55
+ print str
56
+ else
57
+ puts str
58
+ end
59
+ end
60
+
61
+ def indent_level(value)
62
+ case value
63
+ when :incr
64
+ @indent_level = (@indent_level) ? @indent_level + 1 : 0
65
+ when :decr
66
+ @indent_level -= 1
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,37 @@
1
+ require 'singleton'
2
+ require 'colored'
3
+
4
+ module Columnist
5
+ class ProgressFormatter
6
+ include Singleton
7
+ include OptionsValidator
8
+
9
+ VALID_OPTIONS = [:indicator, :color, :bold]
10
+ attr_accessor *VALID_OPTIONS
11
+
12
+ def format(options, block)
13
+ self.validate_options(options, *VALID_OPTIONS)
14
+
15
+ self.indicator = options[:indicator] if options[:indicator]
16
+ self.color = options[:color]
17
+ self.bold = options[:bold] || false
18
+
19
+ block.call
20
+
21
+ puts
22
+ end
23
+
24
+ def progress(override = nil)
25
+ str = override || self.indicator
26
+
27
+ str = str.send(self.color) if self.color
28
+ str = str.send('bold') if self.bold
29
+
30
+ print str
31
+ end
32
+
33
+ def indicator
34
+ @indicator ||= '.'
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ module OptionsValidator
2
+ def validate_options(provided, *allowed_keys)
3
+ raise(ArgumentError, "Valid options: #{allowed_keys}") unless (provided.keys - allowed_keys).empty?
4
+ end
5
+ end
@@ -0,0 +1,83 @@
1
+ module Columnist
2
+ class Row
3
+ include OptionsValidator
4
+
5
+ VALID_OPTIONS = [:header, :color, :border_color, :bold, :encoding]
6
+ attr_accessor :columns, :border, *VALID_OPTIONS
7
+
8
+ def initialize(options = {})
9
+ self.validate_options(options, *VALID_OPTIONS)
10
+
11
+ self.columns = []
12
+ self.border = false
13
+ self.header = options[:header] || false
14
+ self.color = options[:color]
15
+ self.border_color = options[:border_color]
16
+ self.bold = options[:bold] || false
17
+ self.encoding = options[:encoding] || :unicode
18
+ end
19
+
20
+ def add(column)
21
+ if column.color.nil? && self.color
22
+ column.color = self.color
23
+ end
24
+
25
+ if self.bold || self.header
26
+ column.bold = true
27
+ end
28
+
29
+ self.columns << column
30
+ end
31
+
32
+ def output
33
+ screen_count.times do |sr|
34
+ border_char = use_utf8? ? "\u2503" : '|'
35
+ border_char = border_char.send(self.border_color) if self.border_color
36
+
37
+ line = (self.border) ? "#{border_char} " : ''
38
+
39
+ self.columns.size.times do |mc|
40
+ col = self.columns[mc]
41
+ # Account for the fact that some columns will have more screen rows than their
42
+ # counterparts in the row. An example being:
43
+ # c1 = Column.new('x' * 50, :width => 10)
44
+ # c2 = Column.new('x' * 20, :width => 10)
45
+ #
46
+ # c1.screen_rows.size == 5
47
+ # c2.screen_rows.size == 2
48
+ #
49
+ # So when we don't have a screen row for c2 we need to fill the screen with the
50
+ # proper number of blanks so the layout looks like (parenthesis on the right just
51
+ # indicate screen row index)
52
+ #
53
+ # +-------------+------------+
54
+ # | xxxxxxxxxxx | xxxxxxxxxx | (0)
55
+ # | xxxxxxxxxxx | xxxxxxxxxx | (1)
56
+ # | xxxxxxxxxxx | | (2)
57
+ # | xxxxxxxxxxx | | (3)
58
+ # | xxxxxxxxxxx | | (4)
59
+ # +-------------+------------+
60
+ if col.screen_rows[sr].nil?
61
+ line << ' ' * col.width
62
+ else
63
+ line << self.columns[mc].screen_rows[sr]
64
+ end
65
+
66
+ line << ' ' + ((self.border) ? "#{border_char} " : '')
67
+ end
68
+
69
+ puts line
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def screen_count
76
+ @sc ||= self.columns.inject(0) { |max, column| column.screen_rows.size > max ? column.screen_rows.size : max }
77
+ end
78
+
79
+ def use_utf8?
80
+ self.encoding == :unicode && "\u2501" != "u2501"
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,133 @@
1
+ module Columnist
2
+ class Table
3
+ include OptionsValidator
4
+
5
+ VALID_OPTIONS = [:border, :border_color, :width, :encoding]
6
+ attr_accessor :rows, *VALID_OPTIONS
7
+
8
+ def initialize(options = {})
9
+ self.validate_options(options, *VALID_OPTIONS)
10
+
11
+ self.border = options[:border] || false
12
+ self.border_color = options[:border_color] || false
13
+ self.width = options[:width] || false
14
+ self.encoding = options[:encoding] || Columnist::DEFAULTS[:encoding]
15
+
16
+ @rows = []
17
+
18
+ raise ArgumentError, "Invalid encoding" unless [:ascii, :unicode].include? self.encoding
19
+ end
20
+
21
+ def add(row)
22
+ # Inheritance from the table
23
+ row.border = self.border
24
+ row.border_color = self.border_color
25
+
26
+ # Inherit properties from the appropriate row
27
+ inherit_column_attrs(row) if self.rows[0]
28
+
29
+ self.rows << row
30
+ end
31
+
32
+ def output
33
+ return if self.rows.size == 0 # we got here with nothing to print to the screen
34
+ auto_adjust_widths if self.width == :auto
35
+
36
+ puts separator('first') if self.border
37
+ self.rows.each_with_index do |row, index|
38
+ row.output
39
+ puts separator('middle') if self.border && (index != self.rows.size - 1)
40
+ end
41
+ puts separator('last') if self.border
42
+ end
43
+
44
+ def auto_adjust_widths
45
+ column_widths = []
46
+
47
+ self.rows.each do |row|
48
+ row.columns.each_with_index do |col, i|
49
+ column_widths[i] = [col.required_width, (column_widths[i] || 0)].max
50
+ end
51
+ end
52
+
53
+ self.rows.each do |row|
54
+ row.columns.each_with_index do |col, i|
55
+ col.width = column_widths[i]
56
+ end
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def separator(type = 'middle')
63
+ left, center, right, bar = use_utf8? ? utf8_separator(type) : ascii_separator
64
+
65
+ separator_str = left + self.rows[0].columns.map { |c| bar * (c.width + 2) }.join(center) + right
66
+ separator_str.send(self.border_color) if self.border_color
67
+ end
68
+
69
+ def use_utf8?
70
+ self.encoding == :unicode && "\u2501" != "u2501"
71
+ end
72
+
73
+ def ascii_separator
74
+ left = right = center = '+'
75
+ bar = '-'
76
+ [left, right, center, bar]
77
+ end
78
+
79
+ def utf8_separator(type)
80
+ bar = "\u2501"
81
+
82
+ left, center, right = case type
83
+ when 'first'
84
+ ["\u250F", "\u2533", "\u2513"]
85
+ when 'middle'
86
+ ["\u2523", "\u254A", "\u252B"]
87
+ when 'last'
88
+ ["\u2517", "\u253B", "\u251B"]
89
+ end
90
+
91
+ [left, center, right, bar]
92
+ end
93
+
94
+ def inherit_column_attrs(row)
95
+ row.columns.each_with_index do |c, i|
96
+ use_positional_attrs(c, i)
97
+ use_color(row, c, i)
98
+ use_bold(row, c, i)
99
+ end
100
+ end
101
+
102
+ def use_positional_attrs(c, i)
103
+ # The positional attributes are always required to inheret to make sure the table
104
+ # displays properly
105
+ %w{align padding width}.each do |attr|
106
+ val = self.rows[0].columns[i].send(attr)
107
+ c.send(attr + "=", val)
108
+ end
109
+ end
110
+
111
+ def inherit_from
112
+ self.rows[0].header ? 1 : 0
113
+ end
114
+
115
+ def use_color(row, c, i)
116
+ if c.color
117
+ # keep default
118
+ elsif row.color
119
+ c.color = row.color
120
+ elsif inherit_from != 1
121
+ c.color = self.rows[inherit_from].columns[i].color
122
+ end
123
+ end
124
+
125
+ def use_bold(row, c, i)
126
+ if row.bold
127
+ c.bold = row.bold
128
+ elsif inherit_from != 1
129
+ c.bold = self.rows[inherit_from].columns[i].bold
130
+ end
131
+ end
132
+ end
133
+ end