text_layout 0.1.0

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