xmlss 0.4.1 → 1.0.0.rc.1

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.
Files changed (41) hide show
  1. data/Gemfile.lock +3 -3
  2. data/README.rdoc +4 -4
  3. data/bench/profiler_runner.rb +1 -1
  4. data/examples/example_workbook.rb +1 -1
  5. data/examples/simple.rb +18 -1
  6. data/examples/simple.xml +22 -0
  7. data/lib/xmlss/element/cell.rb +52 -1
  8. data/lib/xmlss/element/column.rb +2 -0
  9. data/lib/xmlss/element/row.rb +2 -0
  10. data/lib/xmlss/element/worksheet.rb +14 -6
  11. data/lib/xmlss/element_stack.rb +69 -0
  12. data/lib/xmlss/style/alignment.rb +2 -0
  13. data/lib/xmlss/style/base.rb +2 -0
  14. data/lib/xmlss/style/border.rb +10 -0
  15. data/lib/xmlss/style/font.rb +2 -0
  16. data/lib/xmlss/style/interior.rb +2 -0
  17. data/lib/xmlss/style/number_format.rb +2 -0
  18. data/lib/xmlss/style/protection.rb +2 -0
  19. data/lib/xmlss/version.rb +1 -1
  20. data/lib/xmlss/workbook.rb +75 -58
  21. data/lib/xmlss/writer.rb +244 -0
  22. data/test/element/cell_test.rb +60 -1
  23. data/test/element/column_test.rb +5 -0
  24. data/test/element/row_test.rb +5 -0
  25. data/test/element/worksheet_test.rb +9 -4
  26. data/test/element_stack_test.rb +117 -0
  27. data/test/style/alignment_test.rb +5 -0
  28. data/test/style/base_test.rb +5 -0
  29. data/test/style/border_test.rb +26 -2
  30. data/test/style/font_test.rb +5 -0
  31. data/test/style/interior_test.rb +5 -0
  32. data/test/style/number_format_test.rb +5 -0
  33. data/test/style/protection_test.rb +6 -1
  34. data/test/workbook_test.rb +29 -13
  35. data/test/writer_test.rb +413 -0
  36. data/xmlss.gemspec +1 -1
  37. metadata +30 -25
  38. data/lib/xmlss/element/data.rb +0 -58
  39. data/lib/xmlss/undies_writer.rb +0 -175
  40. data/test/element/data_test.rb +0 -67
  41. data/test/undies_writer_test.rb +0 -372
@@ -0,0 +1,244 @@
1
+ require 'undies'
2
+ require 'stringio'
3
+
4
+ module Xmlss
5
+ class Writer
6
+
7
+ class Markup; end
8
+
9
+ # Xmlss uses Undies to stream its xml markup
10
+ # The Undies writer is responsible for driving the Undies API to generate
11
+ # the xmlss xml markup for the workbook.
12
+ # Because order doesn't matter when defining style and worksheet elements,
13
+ # the writer has to buffer the style and worksheet markup separately,
14
+ # then put them together according to Xmlss spec to build the final
15
+ # workbook markup.
16
+
17
+ XML_NS = "xmlns"
18
+ SHEET_NS = "ss"
19
+ NS_URI = "urn:schemas-microsoft-com:office:spreadsheet"
20
+ LB = "
"
21
+
22
+ def self.attributes(thing, *attrs)
23
+ [*attrs].flatten.inject({}) do |xattrs, a|
24
+ xattrs.merge(if !(xv = self.coerce(thing.send(a))).nil?
25
+ {xmlss_attribute_name(a) => xv.to_s}
26
+ else
27
+ {}
28
+ end)
29
+ end
30
+ end
31
+
32
+ def self.xmlss_attribute_name(attr_name)
33
+ "#{SHEET_NS}:#{self.classify(attr_name)}"
34
+ end
35
+
36
+ def self.classify(underscored_string)
37
+ underscored_string.
38
+ to_s.downcase.
39
+ split("_").
40
+ collect{|part| part.capitalize}.
41
+ join('')
42
+ end
43
+
44
+ def self.coerce(value)
45
+ if value == true
46
+ 1
47
+ elsif ["",false].include?(value)
48
+ # don't include false or empty string values
49
+ nil
50
+ else
51
+ value
52
+ end
53
+ end
54
+
55
+ attr_reader :styles_markup
56
+ attr_reader :worksheets_markup
57
+
58
+ def initialize(output_opts={})
59
+ @opts = output_opts || {}
60
+
61
+ @styles_markup = Markup.new(@opts.merge(:pp_level => 2))
62
+ @worksheets_markup = Markup.new(@opts.merge(:pp_level => 1))
63
+ end
64
+
65
+ def write(element)
66
+ self.send(element.class.writer, element)
67
+ end
68
+
69
+ def push(template)
70
+ self.send("#{template}_markup").push
71
+ end
72
+
73
+ def pop(template)
74
+ self.send("#{template}_markup").pop
75
+ end
76
+
77
+ def flush
78
+ # flush the worksheets markup first b/c it may add style elements to
79
+ # the styles markup
80
+ worksheets_markup.flush
81
+ styles_markup.flush
82
+ self
83
+ end
84
+
85
+ # return the full workbook markup, combining the buffers to xmlss spec
86
+ def workbook
87
+ self.flush
88
+ "".tap do |markup|
89
+ Undies::Template.new(Undies::Source.new(Proc.new do
90
+ __ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
91
+ _Workbook(XML_NS => NS_URI, "#{XML_NS}:#{SHEET_NS}" => NS_URI) {
92
+ _Styles {
93
+ __partial styles
94
+ }
95
+ __partial worksheets
96
+ }
97
+ end), {
98
+ :styles => styles_markup.to_s,
99
+ :worksheets => worksheets_markup.to_s
100
+ }, Undies::Output.new(StringIO.new(markup), @opts))
101
+ end.strip
102
+ end
103
+
104
+ # workbook style markup directives
105
+
106
+ def alignment(alignment)
107
+ styles_markup.template._Alignment(self.class.attributes(alignment, [
108
+ :horizontal, :vertical, :wrap_text, :rotate
109
+ ]))
110
+ end
111
+
112
+ def border(border)
113
+ styles_markup.template._Border(self.class.attributes(border, [
114
+ :color, :position, :weight, :line_style
115
+ ]))
116
+ end
117
+
118
+ def borders(borders)
119
+ styles_markup.template._Borders
120
+ end
121
+
122
+ def font(font)
123
+ styles_markup.template._Font(self.class.attributes(font, [
124
+ :bold, :color, :italic, :size, :shadow, :font_name,
125
+ :strike_through, :underline, :vertical_align
126
+ ]))
127
+ end
128
+
129
+ def interior(interior)
130
+ styles_markup.template._Interior(self.class.attributes(interior, [
131
+ :color, :pattern, :pattern_color
132
+ ]))
133
+ end
134
+
135
+ def number_format(number_format)
136
+ styles_markup.template._NumberFormat(self.class.attributes(number_format, [
137
+ :format
138
+ ]))
139
+ end
140
+
141
+ def protection(protection)
142
+ styles_markup.template._Protection(self.class.attributes(protection, [
143
+ :protect
144
+ ]))
145
+ end
146
+
147
+ def style(style)
148
+ styles_markup.template._Style(self.class.attributes(style, [
149
+ :i_d
150
+ ]))
151
+ end
152
+
153
+ # workbook element markup directives
154
+
155
+ def cell(cell)
156
+ # write the cell markup and push
157
+ worksheets_markup.template._Cell(self.class.attributes(cell, [
158
+ [:index, :style_i_d, :formula, :h_ref, :merge_across, :merge_down]
159
+ ]))
160
+ push(:worksheets)
161
+
162
+ # write nested data markup and push
163
+ worksheets_markup.template._Data(self.class.attributes(cell, [
164
+ [:type]
165
+ ]))
166
+ push(:worksheets)
167
+
168
+ # write data value
169
+ worksheets_markup.template.__ Undies::Template.
170
+ escape_html(cell.data_xml_value).
171
+ gsub(/(\r|\n)+/, LB)
172
+
173
+ pop(:worksheets)
174
+ pop(:worksheets)
175
+ end
176
+
177
+ def row(row)
178
+ worksheets_markup.template._Row(self.class.attributes(row, [
179
+ [:style_i_d, :height, :auto_fit_height, :hidden]
180
+ ]))
181
+ end
182
+
183
+ def column(column)
184
+ worksheets_markup.template._Column(self.class.attributes(column, [
185
+ [:style_i_d, :width, :auto_fit_width, :hidden]
186
+ ]))
187
+ end
188
+
189
+ def worksheet(worksheet)
190
+ # flush any previous worksheet markup
191
+ worksheets_markup.flush
192
+
193
+ # write the worksheet markup and push
194
+ worksheets_markup.template._Worksheet(self.class.attributes(worksheet, [
195
+ [:name]
196
+ ]))
197
+ push(:worksheets)
198
+
199
+ # write the table container
200
+ worksheets_markup.template._Table
201
+ end
202
+
203
+ end
204
+
205
+
206
+
207
+ class Writer::Markup
208
+
209
+ attr_reader :template, :push_count, :pop_count
210
+
211
+ def initialize(opts={})
212
+ @markup = ""
213
+ @template = Undies::Template.new(Undies::Output.new(StringIO.new(@markup), opts))
214
+ @push_count = 0
215
+ @pop_count = 0
216
+ end
217
+
218
+ def push
219
+ @push_count += 1
220
+ @template.__push
221
+ end
222
+
223
+ def pop
224
+ @pop_count += 1
225
+ @template.__pop
226
+ end
227
+
228
+ def flush
229
+ while @push_count > @pop_count
230
+ pop
231
+ end
232
+ @template.__flush
233
+ self
234
+ end
235
+
236
+ def empty?; @markup.empty?; end
237
+
238
+ def to_s
239
+ @markup.to_s.strip
240
+ end
241
+
242
+ end
243
+
244
+ end
@@ -1,22 +1,45 @@
1
1
  require "assert"
2
+ require 'enumeration/assert_macros'
2
3
 
3
4
  require 'xmlss/element/cell'
4
5
 
5
6
  module Xmlss::Element
6
- class CellTest < Assert::Context
7
+
8
+ class CellTests < Assert::Context
9
+ include Enumeration::AssertMacros
10
+
7
11
  desc "Xmlss::Cell"
8
12
  before { @c = Cell.new }
9
13
  subject { @c }
10
14
 
11
15
  should be_styled
16
+ should have_class_method :writer
12
17
  should have_accessor :index, :formula, :href, :merge_across, :merge_down
13
18
  should have_reader :h_ref
14
19
 
20
+ should have_enum :type, {
21
+ :number => "Number",
22
+ :date_time => "DateTime",
23
+ :boolean => "Boolean",
24
+ :string => "String",
25
+ :error => "Error"
26
+ }
27
+
28
+ should have_accessor :data
29
+ should have_instance_method :data_xml_value
30
+
31
+ should "know its writer hook" do
32
+ assert_equal :cell, subject.class.writer
33
+ end
34
+
15
35
  should "set it's defaults" do
16
36
  assert_nil subject.formula
17
37
  assert_nil subject.href
18
38
  assert_nil subject.merge_across
19
39
  assert_nil subject.merge_down
40
+
41
+ assert_equal Cell.type(:string), subject.type
42
+ assert_equal "", subject.data
20
43
  end
21
44
 
22
45
  should "provide alias for :href" do
@@ -60,6 +83,42 @@ module Xmlss::Element
60
83
  end
61
84
  end
62
85
 
86
+ should "generate it's data xml value" do
87
+ assert_equal "12", Cell.new(12).data_xml_value
88
+ assert_equal "string", Cell.new(:data => "string").data_xml_value
89
+ assert_equal "2011-03-01T00:00:00", Cell.new(DateTime.parse('2011/03/01')).data_xml_value
90
+ assert_equal "2011-03-01T00:00:00", Cell.new(Date.parse('2011/03/01')).data_xml_value
91
+ time = Time.now
92
+ assert_equal time.strftime("%Y-%m-%dT%H:%M:%S"), Cell.new(time).data_xml_value
93
+ end
94
+
95
+ end
96
+
97
+ class ExplicitDataTest < CellTests
98
+ desc "when using explicit data type"
99
+ subject do
100
+ Cell.new(12, {:type => :string})
101
+ end
102
+
103
+ should "should ignore the data value's implied type" do
104
+ assert_equal Cell.type(:string), subject.type
105
+ end
106
+
107
+ end
108
+
109
+ class NoTypeDataTest < CellTests
110
+ desc "when no data type is specified"
111
+
112
+ should "cast types for Number, DateTime, Boolean, String" do
113
+ assert_equal Cell.type(:number), Cell.new(12).type
114
+ assert_equal Cell.type(:number), Cell.new(:data => 123.45).type
115
+ assert_equal Cell.type(:date_time), Cell.new(Time.now).type
116
+ assert_equal Cell.type(:boolean), Cell.new(true).type
117
+ assert_equal Cell.type(:boolean), Cell.new(false).type
118
+ assert_equal Cell.type(:string), Cell.new("a string").type
119
+ assert_equal Cell.type(:string), Cell.new(:data => :symbol).type
120
+ end
121
+
63
122
  end
64
123
 
65
124
  end
@@ -9,8 +9,13 @@ module Xmlss::Element
9
9
  subject { @c }
10
10
 
11
11
  should be_styled
12
+ should have_class_method :writer
12
13
  should have_accessor :width, :auto_fit_width, :hidden
13
14
 
15
+ should "know its writer hook" do
16
+ assert_equal :column, subject.class.writer
17
+ end
18
+
14
19
  should "set it's defaults" do
15
20
  assert_equal nil, subject.width
16
21
  assert_equal false, subject.auto_fit_width
@@ -9,8 +9,13 @@ module Xmlss::Element
9
9
  subject { @row }
10
10
 
11
11
  should be_styled
12
+ should have_class_method :writer
12
13
  should have_accessors :height, :auto_fit_height, :hidden
13
14
 
15
+ should "know its writer hook" do
16
+ assert_equal :row, subject.class.writer
17
+ end
18
+
14
19
  should "set it's defaults" do
15
20
  assert_nil subject.height
16
21
  assert_equal false, subject.auto_fit_height
@@ -8,8 +8,13 @@ module Xmlss::Element
8
8
  before { @wksht = Worksheet.new('sheet') }
9
9
  subject { @wksht }
10
10
 
11
+ should have_class_method :writer
11
12
  should have_accessor :name
12
13
 
14
+ should "know its writer hook" do
15
+ assert_equal :worksheet, subject.class.writer
16
+ end
17
+
13
18
  should "set it's defaults" do
14
19
  assert_equal 'sheet', subject.name
15
20
  end
@@ -29,12 +34,12 @@ module Xmlss::Element
29
34
  assert_equal "t[e]st test", ws.name
30
35
  end
31
36
 
32
- should "bark when no name is given" do
37
+ should "complain if given a name longer than 31 chars" do
33
38
  assert_raises ArgumentError do
34
- Worksheet.new(nil)
39
+ Worksheet.new('a'*32)
35
40
  end
36
- assert_raises ArgumentError do
37
- Worksheet.new("")
41
+ assert_nothing_raised do
42
+ Worksheet.new('a'*31)
38
43
  end
39
44
  end
40
45
 
@@ -0,0 +1,117 @@
1
+ require 'assert'
2
+
3
+ require 'xmlss/element_stack'
4
+ require 'xmlss/element/cell'
5
+ require 'xmlss/writer'
6
+
7
+ module Xmlss
8
+
9
+
10
+
11
+ class ElementStackTests < Assert::Context
12
+
13
+ class TestWriter
14
+ def initialize(io); @io = io; end
15
+ def write(element); @io << "|written: #{element.class.name} #{element.object_id}"; end
16
+ def push(template); @io << "|#{template}:opened"; end
17
+ def pop(template); @io << "|#{template}:closed"; end
18
+ end
19
+
20
+ desc "an element stack"
21
+ before do
22
+ @cell1 = Element::Cell.new("1")
23
+ @cell2 = Element::Cell.new("2")
24
+ @es = ElementStack.new(TestWriter.new(@test_io = ''), 'tests')
25
+ end
26
+ subject { @es }
27
+
28
+ should have_reader :markup_type
29
+ should have_instance_methods :current, :size, :level, :empty?, :first, :last
30
+ should have_instance_methods :push, :pop, :using
31
+
32
+ should "be empty by default" do
33
+ assert_empty subject
34
+ end
35
+
36
+ should "know the writer markup_type its driving" do
37
+ assert_equal 'tests', subject.markup_type
38
+ end
39
+
40
+ end
41
+
42
+
43
+
44
+ class StackTests < ElementStackTests
45
+
46
+ should "push elements onto the stack" do
47
+ assert_nothing_raised do
48
+ subject.push(@cell1)
49
+ end
50
+
51
+ assert_equal 1, subject.size
52
+ end
53
+
54
+ should "know its level (should be one less than the array's size)" do
55
+ assert_equal 0, subject.level
56
+ subject.push(@cell1)
57
+ assert_equal 1, subject.level
58
+ end
59
+
60
+ should "fetch the last item in the array with the current method" do
61
+ subject.push(@cell1)
62
+ subject.push(@cell2)
63
+
64
+ assert_same @cell2, subject.current
65
+ end
66
+
67
+ should "remove the last item in the array with the pop method" do
68
+ subject.push(@cell1)
69
+ subject.push(@cell2)
70
+
71
+ assert_equal 2, subject.size
72
+
73
+ elem = subject.pop
74
+ assert_same @cell2, elem
75
+ assert_equal 1, subject.size
76
+ end
77
+
78
+ end
79
+
80
+
81
+
82
+ class WriterTests < ElementStackTests
83
+ should "open the current element element when a new element is pushed" do
84
+ expected = "|written: Xmlss::Element::Cell #{@cell1.object_id}"
85
+ expected += "|#{subject.markup_type}:opened"
86
+ subject.push(@cell1)
87
+ subject.push(@cell2)
88
+
89
+ assert_equal expected, @test_io
90
+ end
91
+
92
+ should "write the current element if no child element is pushed" do
93
+ subject.push(@cell1)
94
+ subject.push(@cell2)
95
+ subject.pop
96
+
97
+ assert_match /\|written: Xmlss::Element::Cell #{@cell2.object_id}$/, @test_io
98
+ end
99
+
100
+ should "close an element when its popped" do
101
+ expected = "|written: Xmlss::Element::Cell #{@cell1.object_id}"
102
+ expected += "|#{subject.markup_type}:opened"
103
+ expected += "|written: Xmlss::Element::Cell #{@cell2.object_id}"
104
+ expected += "|#{subject.markup_type}:closed"
105
+ subject.push(@cell1)
106
+ subject.push(@cell2)
107
+ subject.pop
108
+ subject.pop
109
+
110
+ assert_equal expected, @test_io
111
+ end
112
+
113
+ end
114
+
115
+
116
+
117
+ end
@@ -25,9 +25,14 @@ module Xmlss::Style
25
25
  :bottom => "Bottom"
26
26
  }
27
27
 
28
+ should have_class_method :writer
28
29
  should have_accessors :wrap_text, :rotate
29
30
  should have_instance_methods :wrap_text?
30
31
 
32
+ should "know its writer" do
33
+ assert_equal :alignment, subject.class.writer
34
+ end
35
+
31
36
  should "set it's defaults" do
32
37
  assert_equal false, subject.wrap_text
33
38
  assert_equal nil, subject.horizontal
@@ -8,8 +8,13 @@ module Xmlss::Style
8
8
  before { @bs = Base.new(:test) }
9
9
  subject { @bs }
10
10
 
11
+ should have_class_method :writer
11
12
  should have_reader :id, :i_d
12
13
 
14
+ should "know its writer" do
15
+ assert_equal :style, subject.class.writer
16
+ end
17
+
13
18
  should "bark if you don't init with an id" do
14
19
  assert_raises ArgumentError do
15
20
  Base.new(nil)
@@ -4,7 +4,10 @@ require 'enumeration/assert_macros'
4
4
  require 'xmlss/style/border'
5
5
 
6
6
  module Xmlss::Style
7
- class BorderTest < Assert::Context
7
+
8
+
9
+
10
+ class BorderTests < Assert::Context
8
11
  include Enumeration::AssertMacros
9
12
 
10
13
  desc "Xmlss::Style::Border"
@@ -36,9 +39,13 @@ module Xmlss::Style
36
39
  :dash_dot_dot => "DashDotDot"
37
40
  }
38
41
 
39
-
42
+ should have_class_method :writer
40
43
  should have_accessors :color
41
44
 
45
+ should "know its writer" do
46
+ assert_equal :border, subject.class.writer
47
+ end
48
+
42
49
  should "set it's defaults" do
43
50
  assert_equal nil, subject.color
44
51
  assert_equal nil, subject.position
@@ -85,4 +92,21 @@ module Xmlss::Style
85
92
 
86
93
  end
87
94
 
95
+
96
+
97
+ class BordersTests < Assert::Context
98
+ desc "Xmlss::Style::Borders"
99
+ before { @bs = Xmlss::Style::Borders.new }
100
+ subject { @bs }
101
+
102
+ should have_class_method :writer
103
+
104
+ should "know its writer" do
105
+ assert_equal :borders, subject.class.writer
106
+ end
107
+
108
+ end
109
+
110
+
111
+
88
112
  end
@@ -23,11 +23,16 @@ module Xmlss::Style
23
23
  :superscript => 'Superscript'
24
24
  }
25
25
 
26
+ should have_class_method :writer
26
27
  should have_reader :vertical_align
27
28
  should have_accessors :bold, :color, :italic, :size, :strike_through
28
29
  should have_accessors :shadow, :underline, :alignment, :name
29
30
  should have_instance_methods :bold?, :italic?, :strike_through?, :shadow?
30
31
 
32
+ should "know its writer" do
33
+ assert_equal :font, subject.class.writer
34
+ end
35
+
31
36
  should "set it's defaults" do
32
37
  assert_equal false, subject.bold
33
38
  assert_equal nil, subject.color
@@ -34,8 +34,13 @@ module Xmlss::Style
34
34
  :thin_diag_cross => "ThinDiagCross"
35
35
  }
36
36
 
37
+ should have_class_method :writer
37
38
  should have_accessor :color, :pattern_color
38
39
 
40
+ should "know its writer" do
41
+ assert_equal :interior, subject.class.writer
42
+ end
43
+
39
44
  should "set it's defaults" do
40
45
  assert_equal nil, subject.color
41
46
  assert_equal nil, subject.pattern
@@ -11,8 +11,13 @@ module Xmlss::Style
11
11
  before { @nf = NumberFormat.new }
12
12
  subject { @nf }
13
13
 
14
+ should have_class_method :writer
14
15
  should have_accessor :format
15
16
 
17
+ should "know its writer" do
18
+ assert_equal :number_format, subject.class.writer
19
+ end
20
+
16
21
  should "set attributes at init" do
17
22
  nf = NumberFormat.new("General")
18
23
  assert_equal "General", nf.format
@@ -9,8 +9,13 @@ module Xmlss::Style
9
9
  before { @sp = Xmlss::Style::Protection.new }
10
10
  subject { @sp }
11
11
 
12
- should have_instance_methods :protected?
12
+ should have_class_method :writer
13
13
  should have_accessor :protect
14
+ should have_instance_methods :protected?
15
+
16
+ should "know its writer" do
17
+ assert_equal :protection, subject.class.writer
18
+ end
14
19
 
15
20
  should "set it's defaults" do
16
21
  assert_equal false, subject.protected?