columnist 1.0.0

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