text_layout 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.
data/.gemtest ADDED
File without changes
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 0.1.0 / 2011-09-14
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,8 @@
1
+ History.txt
2
+ lib/text_layout/table.rb
3
+ lib/text_layout/wrap.rb
4
+ lib/text_layout.rb
5
+ Manifest.txt
6
+ Rakefile
7
+ README.rdoc
8
+ test/test_text_layout.rb
data/README.rdoc ADDED
@@ -0,0 +1,83 @@
1
+ = text_layout
2
+
3
+ * http://github.com/nanki/text_layout
4
+
5
+ == DESCRIPTION:
6
+
7
+ Text layout engine for CUI apps.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * Full-width character support.
12
+ * Line wrapping.
13
+ * Table layout with colspan and rowspan.
14
+
15
+ == SYNOPSIS:
16
+
17
+ # (from http://en.wikipedia.org/wiki/MacBook_Air)
18
+ table = [
19
+ [{:colspan => 6, :value => "Table of models"}],
20
+ ["Model", "Early 2008", "Late 2008", "Mid 2009", "Late 2010", "Mid 2011"],
21
+ ["Model Identifier", "MacBookAir1,1", {:colspan=>2, :value => "MacBookAir2,1"}, "MacBookAir3,1(11\")\nMacBookAir3,2(13\")", "MacBookAir4,1(11\")\nMacBookAir4,2(13\")"],
22
+ ["Model number", "MB003LL/A", "MB543LL/A\nMB940LL/A", "MC233LL/A\nMC234LL/A", "MC505LL/A\nMC503LL/A", "MC968ZP/A\nMC969ZP/A\nMC965ZP/A\nMC966ZP/A"],
23
+ [{:rowspan => 2, :value => "Battery"}, {:colspan => 3, :value => "N/A"}, {:colspan => 2, :value => "35Wh(11\")"}],
24
+ [{:colspan => 2, :value => "37Wh(13\")"}, "40Wh(13\")", {:colspan => 2, :value => "50Wh(13\")"}]
25
+ ]
26
+
27
+ puts TextLayout::Table.new(table).layout
28
+
29
+ ---
30
+
31
+ | Table of models |
32
+ | Model | Early 2008 | Late 2008 | Mid 2009 | Late 2010 | Mid 2011 |
33
+ | Model Identifier | MacBookAir1,1 | MacBookAir2,1 | MacBookAir3,1(11") | MacBookAir4,1(11") |
34
+ | | | | MacBookAir3,2(13") | MacBookAir4,2(13") |
35
+ | Model number | MB003LL/A | MB543LL/A | MC233LL/A | MC505LL/A | MC968ZP/A |
36
+ | | | MB940LL/A | MC234LL/A | MC503LL/A | MC969ZP/A |
37
+ | | | | | | MC965ZP/A |
38
+ | | | | | | MC966ZP/A |
39
+ | Battery | N/A | 35Wh(11") |
40
+ | | 37Wh(13") | 40Wh(13") | 50Wh(13") |
41
+
42
+
43
+ == REQUIREMENTS:
44
+
45
+ * unicode-display_width
46
+
47
+ == INSTALL:
48
+
49
+ * $ gem install text_layout
50
+
51
+ == DEVELOPERS:
52
+
53
+ After checking out the source, run:
54
+
55
+ $ rake newb
56
+
57
+ This task will install any missing dependencies, run the tests/specs,
58
+ and generate the RDoc.
59
+
60
+ == LICENSE:
61
+
62
+ (The MIT License)
63
+
64
+ Copyright (c) 2011 FIX
65
+
66
+ Permission is hereby granted, free of charge, to any person obtaining
67
+ a copy of this software and associated documentation files (the
68
+ 'Software'), to deal in the Software without restriction, including
69
+ without limitation the rights to use, copy, modify, merge, publish,
70
+ distribute, sublicense, and/or sell copies of the Software, and to
71
+ permit persons to whom the Software is furnished to do so, subject to
72
+ the following conditions:
73
+
74
+ The above copyright notice and this permission notice shall be
75
+ included in all copies or substantial portions of the Software.
76
+
77
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
78
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
79
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
80
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
81
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
82
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
83
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ Hoe.spec 'text_layout' do
7
+ developer('nanki', 'nanki@dotswitch.net')
8
+ extra_deps << ['unicode-display_width']
9
+ end
10
+
11
+ # vim: syntax=ruby
@@ -0,0 +1,8 @@
1
+ require 'unicode/display_width'
2
+
3
+ module TextLayout
4
+ VERSION = '0.1.0'
5
+
6
+ autoload :Wrap, 'text_layout/wrap'
7
+ autoload :Table, 'text_layout/table'
8
+ end
@@ -0,0 +1,138 @@
1
+ class TextLayout::Table
2
+ def initialize(array)
3
+ @array = array
4
+ end
5
+
6
+ def normalize(cell)
7
+ unless Hash === cell
8
+ cell = {:value => cell}
9
+ end
10
+
11
+ cell[:value] = cell[:value].to_s.lines.map(&:strip)
12
+ cell
13
+ end
14
+
15
+ class Cell < Struct.new(:col, :row, :attr)
16
+ def width
17
+ attr[:value].map(&:display_width).max
18
+ end
19
+
20
+ def height
21
+ attr[:value].size
22
+ end
23
+ end
24
+
25
+ class Span < Cell
26
+ def main?(col, row)
27
+ self.col == col && self.row == row
28
+ end
29
+ end
30
+
31
+ def layout
32
+ spanss = {:row => Hash.new{[]}, :col => Hash.new{[]}}
33
+ layout = @array.map{[]}
34
+
35
+ @array.each_with_index do |cols, row|
36
+ cols.each_with_index do |attr, col|
37
+ attr = normalize(attr)
38
+
39
+ unless col = layout[row].index(nil)
40
+ col = layout[row].size
41
+ end
42
+
43
+ if !attr[:rowspan] && !attr[:colspan]
44
+ layout[row][col] = Cell.new(col, row, attr)
45
+ else
46
+ span = Span.new(col, row, attr)
47
+ spanss[:row][row...row+attr[:rowspan]] <<= span if attr[:rowspan]
48
+ spanss[:col][col...col+attr[:colspan]] <<= span if attr[:colspan]
49
+
50
+ (attr[:rowspan] || 1).times do |rr|
51
+ (attr[:colspan] || 1).times do |cc|
52
+ layout[row+rr][col+cc] = span
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ max = {:row => [], :col => []}
60
+
61
+ layout.each_with_index do |cols, row|
62
+ cols.each_with_index do |cell, col|
63
+ next if Span === cell && !cell.main?(col, row)
64
+
65
+ unless cell.attr[:colspan]
66
+ max[:col][col] = [max[:col][col] || 0, cell.width].max
67
+ end
68
+
69
+ unless cell.attr[:rowspan]
70
+ max[:row][row] = [max[:row][row] || 0, cell.height].max
71
+ end
72
+ end
73
+ end
74
+
75
+ [[:col, 3], [:row, 0]].each do |dir, margin|
76
+ spanss[dir].each do |range, spans|
77
+ range_size = range.end - range.begin
78
+ spans.each do |span|
79
+ size = sum(max[dir][range]) + margin * (range_size - 1)
80
+ diff = (dir == :col ? span.width : span.height) - size
81
+
82
+ next unless diff > 0
83
+ q, r = diff.divmod range_size
84
+ range.each_with_index do |i, rr|
85
+ max[dir][i] += q + (rr < r ? 1 : 0)
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ result = ""
92
+ sum(max[:row]).times do |layout_row|
93
+ n = layout_row
94
+ row = max[:row].each_with_index do |height, i|
95
+ if n - height >= 0
96
+ n -= height
97
+ else
98
+ break i
99
+ end
100
+ end
101
+
102
+ line = []
103
+ layout[row].each_with_index do |cell, col|
104
+ next unless cell.col == col
105
+ n = layout_row - sum(max[:row][0...cell.row])
106
+ width =
107
+ if Span === cell && colspan = cell.attr[:colspan]
108
+ sum(max[:col][col...col+colspan]) + 3 * (colspan - 1)
109
+ else
110
+ max[:col][col]
111
+ end
112
+
113
+ line << justify(cell.attr[:value][n].to_s, width)
114
+ end
115
+
116
+ result += "| " + line.join(" | ") + " |\n"
117
+ end
118
+
119
+ result
120
+ end
121
+
122
+ def sum(array)
123
+ array.inject(0) {|r, i| r + i }
124
+ end
125
+
126
+ def justify(str, width, type=:right)
127
+ pad = width - str.display_width
128
+ pad = 0 if pad < 0
129
+ case type
130
+ when :right
131
+ " " * pad + str
132
+ when :left
133
+ str + " " * pad
134
+ when :center
135
+ " " * (pad / 2) + str + " " * (pad - pad / 2)
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,51 @@
1
+ class TextLayout::Wrap
2
+ def initialize(str)
3
+ @str = str
4
+ end
5
+
6
+ def layout(width=80)
7
+ raise "width must be greater than 1" if width < 2
8
+ words = @str.scan(/[[:alnum:]]+|./)
9
+
10
+ lines = []
11
+ line = ''
12
+ while word = words.shift
13
+ word.lstrip! if line.empty?
14
+ rest_width = width - line.display_width
15
+
16
+ if word.display_width <= rest_width
17
+ line += word
18
+ flush = line.display_width == width
19
+ else
20
+ word, rest = split_word(word, rest_width - 1)
21
+ line += word + "-" unless word.empty?
22
+ words.unshift rest
23
+ flush = true
24
+ end
25
+
26
+ if flush
27
+ lines << line
28
+ line = ''
29
+ end
30
+ end
31
+
32
+ lines << line
33
+
34
+ lines.pop if lines.last.empty?
35
+
36
+ lines.join("\n")
37
+ end
38
+
39
+ private
40
+ def split_word(word, width)
41
+ if word.display_width < width
42
+ [word, '']
43
+ else
44
+ i, dw = 0, 0
45
+ chars = word.chars.to_a
46
+ dw += chars[i += 1].display_width while dw < width
47
+ i -= 1 if dw > width
48
+ [chars[0...i].join, chars[i..-1].join]
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,61 @@
1
+ $: << '../lib';require 'rubygems';$KCODE = 'u'
2
+ require "test/unit"
3
+ require "text_layout"
4
+
5
+ class TestTextLayout < Test::Unit::TestCase
6
+ def test_wrap_private
7
+ wrap = TextLayout::Wrap.new("")
8
+ assert_equal %w(linew rap), wrap.send(:split_word, "linewrap", 5)
9
+ assert_equal %w(あい うえ), wrap.send(:split_word, "あいうえ", 5)
10
+ end
11
+
12
+ def test_wrap
13
+ assert_equal (["+"*20] * 4).join("\n"), TextLayout::Wrap.new("+"*80).layout(20)
14
+ assert_equal "line-\nwrap", TextLayout::Wrap.new("linewrap").layout(5)
15
+ end
16
+
17
+ def test_table_simple
18
+ assert_table [
19
+ [1, 2],
20
+ [3, 4]
21
+ ], <<-RESULT
22
+ | 1 | 2 |
23
+ | 3 | 4 |
24
+ RESULT
25
+ end
26
+
27
+ def test_table_multipleline
28
+ assert_table [
29
+ ["1\n2", 2],
30
+ [3, 4]
31
+ ], <<-RESULT
32
+ | 1 | 2 |
33
+ | 2 | |
34
+ | 3 | 4 |
35
+ RESULT
36
+ end
37
+
38
+ def test_table_span
39
+ assert_table [
40
+ [{:value => 1, :rowspan => 2}, 2, 3],
41
+ [{:value => 4, :colspan => 2}]
42
+ ], <<-RESULT
43
+ | 1 | 2 | 3 |
44
+ | | 4 |
45
+ RESULT
46
+ end
47
+
48
+ def test_expand_multicolumn
49
+ assert_table [
50
+ [1, 2, 3],
51
+ [4, {:value => "too loooooong", :colspan => 2}]
52
+ ], <<-RESULT
53
+ | 1 | 2 | 3 |
54
+ | 4 | too loooooong |
55
+ RESULT
56
+ end
57
+
58
+ def assert_table(array, result)
59
+ assert_equal result, TextLayout::Table.new(array).layout
60
+ end
61
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: text_layout
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - nanki
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-09-14 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: unicode-display_width
16
+ requirement: &70131146946840 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70131146946840
25
+ - !ruby/object:Gem::Dependency
26
+ name: hoe
27
+ requirement: &70131146946300 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '2.10'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70131146946300
36
+ description: ''
37
+ email:
38
+ - nanki@dotswitch.net
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files:
42
+ - History.txt
43
+ - Manifest.txt
44
+ files:
45
+ - History.txt
46
+ - lib/text_layout/table.rb
47
+ - lib/text_layout/wrap.rb
48
+ - lib/text_layout.rb
49
+ - Manifest.txt
50
+ - Rakefile
51
+ - README.rdoc
52
+ - test/test_text_layout.rb
53
+ - .gemtest
54
+ homepage:
55
+ licenses: []
56
+ post_install_message:
57
+ rdoc_options:
58
+ - --main
59
+ - README.txt
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project: text_layout
76
+ rubygems_version: 1.8.6
77
+ signing_key:
78
+ specification_version: 3
79
+ summary: ''
80
+ test_files:
81
+ - test/test_text_layout.rb