text_layout 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gemtest +0 -0
- data/History.txt +6 -0
- data/Manifest.txt +8 -0
- data/README.rdoc +83 -0
- data/Rakefile +11 -0
- data/lib/text_layout.rb +8 -0
- data/lib/text_layout/table.rb +138 -0
- data/lib/text_layout/wrap.rb +51 -0
- data/test/test_text_layout.rb +61 -0
- metadata +81 -0
data/.gemtest
ADDED
File without changes
|
data/History.txt
ADDED
data/Manifest.txt
ADDED
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
data/lib/text_layout.rb
ADDED
@@ -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
|