eideticrml 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +55 -0
  3. data/lib/erml.rb +345 -0
  4. data/lib/erml_layout_managers.rb +667 -0
  5. data/lib/erml_rules.rb +104 -0
  6. data/lib/erml_styles.rb +304 -0
  7. data/lib/erml_support.rb +105 -0
  8. data/lib/erml_widget_factories.rb +38 -0
  9. data/lib/erml_widgets.rb +1895 -0
  10. data/samples/test10_rich_text.erml +17 -0
  11. data/samples/test11_table_layout.erml +30 -0
  12. data/samples/test12_shapes.erml +32 -0
  13. data/samples/test13_polygons.erml +28 -0
  14. data/samples/test14_images.erml +19 -0
  15. data/samples/test15_lines.erml +43 -0
  16. data/samples/test16_classes.erml +34 -0
  17. data/samples/test17_rules.erml +24 -0
  18. data/samples/test18_preformatted_text.erml +9 -0
  19. data/samples/test19_erb.erml.erb +26 -0
  20. data/samples/test1_empty_doc.erml +2 -0
  21. data/samples/test20_haml.erml.haml +20 -0
  22. data/samples/test21_shift_widgets.erml +47 -0
  23. data/samples/test22_multipage_flow_layout.erml +40 -0
  24. data/samples/test23_pageno.erml +17 -0
  25. data/samples/test24_headers_footers.erml.erb +37 -0
  26. data/samples/test25_overflow.erml.erb +37 -0
  27. data/samples/test26_columns.erml.erb +42 -0
  28. data/samples/test28_landscape.erml.erb +17 -0
  29. data/samples/test29_pages_up.erml.erb +17 -0
  30. data/samples/test2_empty_page.erml +6 -0
  31. data/samples/test30_encodings.erml.haml +35 -0
  32. data/samples/test3_hello_world.erml +7 -0
  33. data/samples/test4_two_pages.erml +10 -0
  34. data/samples/test5_rounded_rect.erml +10 -0
  35. data/samples/test6_bullets.erml +16 -0
  36. data/samples/test7_flow_layout.erml +20 -0
  37. data/samples/test8_vbox_layout.erml +23 -0
  38. data/samples/test9_hbox_layout.erml +22 -0
  39. data/samples/testimg.jpg +0 -0
  40. data/test/test_erml_layout_managers.rb +106 -0
  41. data/test/test_erml_rules.rb +116 -0
  42. data/test/test_erml_styles.rb +415 -0
  43. data/test/test_erml_support.rb +140 -0
  44. data/test/test_erml_widget_factories.rb +46 -0
  45. data/test/test_erml_widgets.rb +1235 -0
  46. data/test/test_helpers.rb +18 -0
  47. metadata +102 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5e7a24549e78789d378487d0b3b2d7c8ae015167
4
+ data.tar.gz: de5a5090642ccb6928f0b31f301e8583e7c895df
5
+ SHA512:
6
+ metadata.gz: 56da9c4a8f20131617bba41606eb6f4ae8293ea5ee6c7375b5c7266ee766ba6ab71c26050f5e871af2d517e88890de165ec8ab58cae82f3bbbab3b13469ee174
7
+ data.tar.gz: ba2c27d259d1ba7eb4275eeb59878f5b887c7c696fd5249459a142fc3dfb3692c346761d29bd0142577dfd0b4efaa738ce9b0ab44e8bfd29b3a826385a795430
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rubygems/package_task'
3
+ require 'rake/testtask'
4
+
5
+ spec = Gem::Specification.new do |s|
6
+ s.name = "eideticrml"
7
+ s.version = "0.3.0"
8
+ s.date = "2017-09-11"
9
+ s.summary = "Report Markup Language"
10
+ s.requirements = "Ruby 2.x, eideticpdf"
11
+ s.add_runtime_dependency "eideticpdf", [">= 1.0.2"]
12
+ s.require_path = 'lib'
13
+ s.autorequire = 'erml'
14
+ s.email = "brent.rowland@eideticsoftware.com"
15
+ s.homepage = "http://www.eideticsoftware.com"
16
+ s.author = "Brent Rowland, Eidetic Software, LLC"
17
+ # s.rubyforge_project = "eideticrml"
18
+ # s.test_file = "test/pdf_tests.rb"
19
+ s.has_rdoc = false
20
+ # s.extra_rdoc_files = ['README']
21
+ # s.rdoc_options << '--title' << 'Eidetic RML' << '--main' << 'README' << '-x' << 'test'
22
+ s.files = FileList["lib/*.rb"] + ['Rakefile'] + FileList["test/test*.rb"] + FileList["samples/test*.erml*"] + ['samples/testimg.jpg']
23
+ s.platform = Gem::Platform::RUBY
24
+ end
25
+
26
+ Gem::PackageTask.new(spec) do |pkg|
27
+ pkg.need_tar = true
28
+ pkg.need_zip = true
29
+ end
30
+
31
+ Rake::TestTask.new do |t|
32
+ t.test_files = FileList['test/test*.rb']
33
+ t.verbose = true
34
+ end
35
+
36
+ desc "Clean up files generated by tests."
37
+ task :clean do
38
+ rm Dir["*.pdf"]
39
+ rm Dir["samples/*.pdf"]
40
+ rm Dir["test/*.pdf"]
41
+ end
42
+
43
+ desc "Render test erml files to pdf."
44
+ task :ermls do
45
+ start = Time.now
46
+ pdfs = []
47
+ require_relative 'lib/erml'
48
+ Dir["samples/*.erml","samples/*.haml","samples/*.erb"].each do |erml|
49
+ puts erml
50
+ pdfs << render_erml(erml)
51
+ end
52
+ elapsed = Time.now - start
53
+ puts "Elapsed: #{(elapsed * 1000).round} ms"
54
+ `open -a Preview #{pdfs * ' '}` if (RUBY_PLATFORM =~ /darwin/) and ($0 !~ /rake_test_loader/)
55
+ end
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brent Rowland on 2008-01-06.
4
+ # Copyright (c) 2008 Eidetic Software. All rights reserved.
5
+
6
+ $:.unshift File.expand_path(File.dirname(__FILE__))
7
+ require 'erml_support'
8
+ require 'erml_widgets'
9
+
10
+ module EideticRML
11
+ STANDARD_ALIASES = [
12
+ { 'id' => 'h', 'tag' => 'p', 'font.weight' => 'Bold', 'text_align' => 'center', 'width' => '100%' }.freeze,
13
+ { 'id' => 'b', 'tag' => 'span', 'font.weight' => 'Bold' }.freeze,
14
+ { 'id' => 'i', 'tag' => 'span', 'font.style' => 'Italic' }.freeze,
15
+ { 'id' => 'u', 'tag' => 'span', 'underline' => 'true' }.freeze,
16
+ { 'id' => 'hbox', 'tag' => 'div', 'layout' => 'hbox' }.freeze,
17
+ { 'id' => 'vbox', 'tag' => 'div', 'layout' => 'vbox' }.freeze,
18
+ { 'id' => 'table', 'tag' => 'div', 'layout' => 'table' }.freeze,
19
+ { 'id' => 'layer', 'tag' => 'div', 'position' => 'relative', 'width' => '100%', 'height' => '100%' }.freeze,
20
+ { 'id' => 'br', 'tag' => 'label' }.freeze
21
+ ]
22
+
23
+ class StyleBuilder
24
+ def initialize(styles)
25
+ @styles = styles
26
+ end
27
+
28
+ def method_missing(id, *args)
29
+ @styles.add(id, *args)
30
+ end
31
+ end
32
+
33
+ class RuleBuilder
34
+ def initialize(rules)
35
+ @rules = rules
36
+ end
37
+
38
+ def rule(selector, attrs={})
39
+ @rules.add(selector, attrs)
40
+ end
41
+ end
42
+
43
+ class PageBuilder
44
+ undef_method :p
45
+
46
+ def initialize(doc)
47
+ @stack = [doc]
48
+ @tag_aliases = {}
49
+ EideticRML::STANDARD_ALIASES.each do |a|
50
+ a = a.dup
51
+ define(a.delete('id'), a['tag'], a)
52
+ end
53
+ end
54
+
55
+ def initialize_copy(other)
56
+ @stack = @stack.clone
57
+ end
58
+
59
+ def method_missing(id, *args, &block)
60
+ if current.respond_to?(id)
61
+ current.send(id, *args)
62
+ return current
63
+ else
64
+ tag, attrs = @tag_aliases[id.to_s] || [id.to_s, {}]
65
+ attrs = attrs.merge(fix_attrs(args.first))
66
+ attrs['tag'] = id unless tag == id
67
+ factory = Widgets::StdWidgetFactory.instance # TODO: select factory by namespace
68
+ raise ArgumentError, "Unknown tag: #{tag}." unless factory.has_widget?(tag)
69
+ widget = factory.make_widget(tag, current, attrs)
70
+ @stack.push(widget)
71
+ result = if block_given?
72
+ yield
73
+ current
74
+ else
75
+ self.clone
76
+ end
77
+ @stack.pop
78
+ return result
79
+ end
80
+ end
81
+
82
+ def define(new_tag, old_tag, attrs)
83
+ @tag_aliases[new_tag.to_s] = [old_tag.to_s, attrs.clone.freeze]
84
+ end
85
+
86
+ private
87
+ def current
88
+ @stack.last
89
+ end
90
+
91
+ def fix_attrs(attrs)
92
+ if attrs.nil?
93
+ {}
94
+ elsif attrs.respond_to?(:to_str)
95
+ { :text => attrs }
96
+ else
97
+ attrs
98
+ end
99
+ end
100
+ end
101
+
102
+ class Builder
103
+ attr_reader :doc
104
+
105
+ def initialize(&block)
106
+ @doc = Widgets::Document.new
107
+ self.instance_eval(&block) if block_given?
108
+ end
109
+
110
+ def styles(&block)
111
+ @styles ||= StyleBuilder.new(@doc.styles)
112
+ @styles.instance_eval(&block) if block_given?
113
+ @styles
114
+ end
115
+
116
+ def rules(&block)
117
+ @rules ||= RuleBuilder.new(@doc.rules)
118
+ @rules.instance_eval(&block)
119
+ end
120
+
121
+ def pages(attrs={}, &block)
122
+ @doc.attributes(attrs)
123
+ @pages ||= PageBuilder.new(@doc)
124
+ @pages.instance_eval(&block) if block_given?
125
+ @pages
126
+ end
127
+
128
+ def print(options={})
129
+ file = options[:file] || "#{File.basename($0, '.rb')}.pdf"
130
+ File.open(file,'w') { |f| f.write(@doc) }
131
+ end
132
+ end
133
+
134
+ class XmlStyleParser
135
+ undef_method :p
136
+
137
+ def initialize(stack, styles)
138
+ @stack, @styles = stack, styles
139
+ @stack.push styles
140
+ end
141
+
142
+ def method_missing(id, *args)
143
+ attrs = args.first.inject({}) { |attrs, kv| attrs[kv[0].to_sym] = kv[1]; attrs }
144
+ @styles.add(id, attrs)
145
+ @stack.push @styles
146
+ end
147
+
148
+ def text(text)
149
+ # no meaningful text in styles section
150
+ end
151
+ end
152
+
153
+ class XmlRuleParser
154
+ undef_method :p
155
+ undef_method :rule if self.private_methods.include?('rule')
156
+
157
+ def initialize(stack, rules)
158
+ @stack, @rules = stack, rules
159
+ @stack.push rules
160
+ end
161
+
162
+ def comment(text)
163
+ # puts "rule comment: #{text}"
164
+ Rules::Rule.parse(text).each { |rule| @rules.add(rule[0], rule[1]) }
165
+ end
166
+
167
+ def method_missing(id, *args)
168
+ @stack.push @rules.add(id, *args)
169
+ end
170
+
171
+ def text(text)
172
+ # no meaningful text in rules section
173
+ end
174
+ end
175
+
176
+ class XmlPageParser
177
+ COLLISIONS = [:p, :method]
178
+ COLLISIONS.each { |symbol| undef_method(symbol) }
179
+
180
+ def initialize(stack, doc)
181
+ @stack = stack
182
+ @stack.push doc
183
+ @tag_aliases = {}
184
+ STANDARD_ALIASES.each { |definition| define(definition) }
185
+ end
186
+
187
+ def method_missing(id, *args)
188
+ # puts "page method_missing: #{id}, #{args.inspect}"
189
+ if current.respond_to?(id) and !COLLISIONS.include?(id)
190
+ current.send(id, *args)
191
+ @stack.push(current)
192
+ else
193
+ tag, attrs = @tag_aliases[id.to_s] || [id.to_s, {}]
194
+ attrs = attrs.merge(args.first)
195
+ attrs['tag'] = id unless tag == id
196
+ factory = Widgets::StdWidgetFactory.instance # TODO: select factory by namespace
197
+ raise ArgumentError, "Unknown tag: #{tag}." unless factory.has_widget?(tag)
198
+ # puts "Making #{tag} with parent #{current.class}."
199
+ widget = factory.make_widget(tag, current, attrs)
200
+ @stack.push(widget)
201
+ end
202
+ end
203
+
204
+ def define(attrs)
205
+ attrs = attrs.dup
206
+ id, tag = attrs.delete('id').to_s, attrs['tag'].to_s
207
+ raise ArgumentError, "Invalid id for define: #{id}." unless id =~ /^(\w+)$/
208
+ raise ArgumentError, "Invalid tag for define: #{tag}." unless tag =~ /^(\w+)$/
209
+ @tag_aliases[id] = [tag, attrs.freeze]
210
+ @stack.push(current)
211
+ end
212
+
213
+ def text(text)
214
+ if current.respond_to?(:text)
215
+ current.text(text)
216
+ end
217
+ end
218
+
219
+ private
220
+ def current
221
+ @stack.last
222
+ end
223
+ end
224
+
225
+ class XmlParser
226
+ attr_reader :doc
227
+
228
+ def initialize
229
+ @doc = Widgets::Document.new
230
+ end
231
+
232
+ def self.parse(data)
233
+ require 'rexml/document'
234
+ parser = self.new
235
+ REXML::Document.parse_stream(data, parser)
236
+ parser.doc
237
+ end
238
+
239
+ def comment(text)
240
+ # puts "base comment: #{text}"
241
+ @parser.comment(text) if @parser.respond_to?(:comment)
242
+ end
243
+
244
+ def tag_start(name, attrs)
245
+ # puts "tag_start: #{name}"
246
+ if @parser.nil?
247
+ self.send(name, attrs)
248
+ else
249
+ @parser.send(name, attrs)
250
+ end
251
+ rescue Exception => e
252
+ raise ArgumentError,
253
+ "Error processing <%s>\n%s" % [attrs.inject(name) { |tag, (k, v)| tag << " #{k}=\"#{v}\"" }, e.message], e.backtrace
254
+ end
255
+
256
+ def tag_end(name)
257
+ # puts "tag_end: #{name}"
258
+ @stack.pop
259
+ @parser = nil if @stack.empty?
260
+ end
261
+
262
+ def text(text)
263
+ # puts "text: #{text.strip}"
264
+ @parser.text(text) unless @parser.nil?
265
+ end
266
+
267
+ def method_missing(id, *args)
268
+ # puts "missing: #{id}, #{args.inspect}"
269
+ end
270
+
271
+ private
272
+ def current
273
+ @stack.last
274
+ end
275
+
276
+ def erml(attrs)
277
+ @stack = []
278
+ end
279
+
280
+ def styles(attrs)
281
+ # puts "styles"
282
+ @parser = XmlStyleParser.new(@stack, @doc.styles)
283
+ end
284
+
285
+ def rules(attrs)
286
+ # puts "rules"
287
+ @parser = XmlRuleParser.new(@stack, @doc.rules)
288
+ if url = attrs['url']
289
+ text = open(url) { |f| f.read }
290
+ @parser.comment(text)
291
+ end
292
+ end
293
+
294
+ def pages(attrs)
295
+ # puts "pages"
296
+ @doc.attributes(attrs)
297
+ @parser = XmlPageParser.new(@stack, @doc)
298
+ end
299
+ end
300
+ end
301
+
302
+ def open_erml(erml, &block)
303
+ if erml =~ /\.erb$/
304
+ require 'erb'
305
+ require 'stringio'
306
+ source = open(erml) { |f| f.read }
307
+ result = ERB.new(source).result
308
+ sio = StringIO.new(result)
309
+ yield(sio)
310
+ elsif erml =~ /\.haml$/
311
+ require 'haml'
312
+ require 'stringio'
313
+ source = open(erml) { |f| f.read }
314
+ result = Haml::Engine.new(source).render
315
+ sio = StringIO.new(result)
316
+ yield(sio)
317
+ else
318
+ File.open(erml, &block)
319
+ end
320
+ end
321
+
322
+ def render_erml(erml)
323
+ doc = open_erml(erml) do |f|
324
+ begin
325
+ EideticRML::XmlParser.parse(f)
326
+ rescue Exception => e
327
+ $stderr.puts "Error in %s: %s\n%s" % [erml, e.message, e.backtrace.join("\n")]
328
+ end
329
+ end
330
+ unless doc.nil?
331
+ pdf = erml.sub(/\.erml(\.erb|\.haml)?$/, '') << '.pdf'
332
+ File.open(pdf, 'w') { |f| f.write(doc) }
333
+ return pdf
334
+ end
335
+ end
336
+
337
+ # ARGV.unshift "samples/test24.erml.erb" unless ARGV.size.nonzero?
338
+ if $0 == __FILE__ and erml = ARGV.shift and File.exist?(erml)
339
+ begin
340
+ pdf = render_erml(erml)
341
+ `open -a Preview #{pdf}` if pdf and (RUBY_PLATFORM =~ /darwin/) and ($0 !~ /rake_test_loader/ and $0 !~ /rcov/)
342
+ rescue Exception => e
343
+ $stderr.puts e.message, e.backtrace.join("\n")
344
+ end
345
+ end
@@ -0,0 +1,667 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brent Rowland on 2008-01-06.
4
+ # Copyright (c) 2008 Eidetic Software. All rights reserved.
5
+
6
+ require 'erml_support'
7
+
8
+ module EideticRML
9
+ module LayoutManagers
10
+ class LayoutManager
11
+ def initialize(style)
12
+ @style = style
13
+ end
14
+
15
+ def row_grid(container)
16
+ grid = Support::Grid.new(container.children.size, 1)
17
+ container.children.each_with_index do |widget, index|
18
+ grid[index, 0] = widget
19
+ end
20
+ grid
21
+ end
22
+
23
+ def col_grid(container)
24
+ grid = Support::Grid.new(1, container.children.size)
25
+ container.children.each_with_index do |widget, index|
26
+ grid[0, index] = widget
27
+ end
28
+ grid
29
+ end
30
+
31
+ def layout(container, writer)
32
+ absolute_widgets = container.children.select { |widget| widget.position == :absolute }
33
+ layout_absolute(container, writer, absolute_widgets)
34
+ relative_widgets = container.children.select { |widget| widget.position == :relative }
35
+ layout_relative(container, writer, relative_widgets)
36
+ container.children.each { |widget| container.root_page.positioned_widgets[widget.position] += 1 if widget.visible and widget.leaf? }
37
+ # $stderr.puts "+++base+++ #{container.root_page.positioned_widgets[:static]}"
38
+ end
39
+
40
+ def layout_absolute(container, writer, widgets)
41
+ widgets.each do |widget|
42
+ widget.before_layout
43
+ widget.position(:absolute)
44
+ widget.left(0, :pt) if widget.left.nil? and widget.right.nil?
45
+ widget.top(0, :pt) if widget.top.nil? and widget.bottom.nil?
46
+ widget.width(widget.preferred_width(writer), :pt) if widget.width.nil?
47
+ widget.height(widget.preferred_height(writer), :pt) if widget.height.nil? # swapped
48
+ widget.layout_widget(writer) # swapped
49
+ end
50
+ end
51
+
52
+ def layout_relative(container, writer, widgets)
53
+ widgets.each do |widget|
54
+ widget.before_layout
55
+ widget.position(:relative) if widget.position == :static
56
+ widget.left(0, :pt) if widget.left.nil? and widget.right.nil?
57
+ widget.top(0, :pt) if widget.top.nil? and widget.bottom.nil?
58
+ widget.width(widget.preferred_width(writer), :pt) if widget.width.nil?
59
+ widget.layout_widget(writer)
60
+ widget.height(widget.preferred_height(writer), :pt) if widget.height.nil?
61
+ end
62
+ end
63
+
64
+ def after_layout(container)
65
+ end
66
+
67
+ def self.register(name, klass)
68
+ (@@klasses ||= {})[name] = klass
69
+ end
70
+
71
+ def self.for_name(name)
72
+ @@klasses[name] unless @@klasses.nil?
73
+ end
74
+
75
+ protected
76
+ def printable_widgets(container, position)
77
+ dpgno, spgno = container.root.document_page_no, container.root.section_page_no
78
+ widgets, remaining = container.children.partition do |child|
79
+ (child.position == :static) and (!child.printed or child.display_for_page(dpgno, spgno))
80
+ end
81
+ end
82
+ end
83
+
84
+ class AbsoluteLayout < LayoutManager
85
+ register('absolute', self)
86
+
87
+ alias :grid :row_grid
88
+
89
+ def layout(container, writer)
90
+ layout_absolute(container, writer, container.children)
91
+ end
92
+
93
+ def preferred_height(grid, writer)
94
+ return grid.row(0).empty? ? 0 : nil
95
+ end
96
+
97
+ def preferred_width(grid, writer)
98
+ return grid.row(0).empty? ? 0 : nil
99
+ end
100
+ end
101
+
102
+ class FlowLayout < LayoutManager
103
+ register('flow', self)
104
+
105
+ alias :grid :row_grid
106
+
107
+ def layout(container, writer)
108
+ cx = cy = max_y = 0
109
+ container_full = false
110
+ bottom = container.content_top + container.max_content_height
111
+ widgets, remaining = printable_widgets(container, :static)
112
+ remaining.each { |widget| widget.visible = false if widget.printed }
113
+ widgets.each do |widget|
114
+ widget.visible = !container_full
115
+ next if container_full
116
+ widget.before_layout
117
+ widget.width([widget.preferred_width(writer) || container.content_width, container.content_width].min, :pt) if widget.width.nil?
118
+ # puts "flow widget width: #{widget.width} #{widget.path}"
119
+ if cx != 0 and cx + widget.width > container.content_width
120
+ cy += max_y + @style.vpadding
121
+ cx = max_y = 0
122
+ end
123
+ widget.left(container.content_left + cx, :pt)
124
+ widget.top(container.content_top + cy, :pt)
125
+ widget.height(widget.preferred_height(writer) || 0, :pt) if widget.height.nil? # swapped
126
+ widget.layout_widget(writer) # swapped
127
+ # if container.bottom and widget.bottom > container.bottom
128
+ if widget.bottom > bottom
129
+ container_full = true
130
+ # widget.visible = (cy == 0)
131
+ widget.visible = container.root_page.positioned_widgets[:static] == 0
132
+ # $stderr.puts "+++flow+++ #{container.root_page.positioned_widgets[:static]}, visible: #{widget.visible}"
133
+ next
134
+ end
135
+ container.root_page.positioned_widgets[widget.position] += 1
136
+ cx += widget.width + @style.hpadding
137
+ max_y = [max_y, widget.height].max
138
+ end
139
+ container.more(true) if container_full and container.overflow
140
+ container.height(cy + max_y + container.non_content_height, :pt) if container.height.nil? and max_y > 0
141
+ super(container, writer)
142
+ end
143
+
144
+ def preferred_height(grid, writer)
145
+ cells = grid.row(0)
146
+ return 0 if cells.empty?
147
+ cell_heights = cells.map { |w| w.preferred_height(writer) }
148
+ return nil unless cell_heights.all?
149
+ cell_heights.max
150
+ end
151
+
152
+ def preferred_width(grid, writer)
153
+ cells = grid.row(0)
154
+ return 0 if cells.empty?
155
+ cell_widths = cells.map { |w| w.preferred_width(writer) }
156
+ return nil unless cell_widths.all?
157
+ cell_widths.inject((cells.size - 1) * @style.hpadding) { |sum, width| sum + width }
158
+ end
159
+ end
160
+
161
+ class HBoxLayout < LayoutManager
162
+ register('hbox', self)
163
+
164
+ alias :grid :row_grid
165
+
166
+ def layout(container, writer)
167
+ container_full = false
168
+ widgets, remaining = printable_widgets(container, :static)
169
+ remaining.each { |widget| widget.visible = false if widget.printed }
170
+ static, relative = widgets.partition { |widget| widget.position == :static }
171
+ lpanels, unaligned = static.partition { |widget| widget.align == :left }
172
+ rpanels, unaligned = unaligned.partition { |widget| widget.align == :right }
173
+ percents, others = static.partition { |widget| widget.width_pct }
174
+ specified, others = others.partition { |widget| widget.width }
175
+
176
+ width_avail = container.content_width
177
+
178
+ # allocate specified widths first
179
+ specified.each do |widget|
180
+ width_avail -= widget.width
181
+ container_full = width_avail < 0
182
+ widget.disabled = container_full
183
+ width_avail -= @style.hpadding
184
+ end
185
+
186
+ # allocate percent widths next, with a minimum width of 1 point
187
+ if width_avail - (percents.size - 1) * @style.hpadding >= percents.size
188
+ width_avail -= (percents.size - 1) * @style.hpadding
189
+ total_percents = percents.inject(0) { |total, widget| total + widget.width }
190
+ ratio = width_avail.quo(total_percents)
191
+ percents.each do |widget|
192
+ widget.width(widget.width * ratio, :pt) if ratio < 1.0
193
+ width_avail -= widget.width
194
+ end
195
+ else
196
+ container_full = true
197
+ percents.each { |widget| widget.disabled = true }
198
+ end
199
+ width_avail -= @style.hpadding
200
+
201
+ # divide remaining width equally among widgets with unspecified widths
202
+ if width_avail - (others.size - 1) * @style.hpadding >= others.size
203
+ width_avail -= (others.size - 1) * @style.hpadding
204
+ others_width = width_avail.quo(others.size)
205
+ others.each { |widget| widget.width(others_width, :pt) }
206
+ else
207
+ container_full = true
208
+ others.each { |widget| widget.disabled = true }
209
+ end
210
+
211
+ static.each do |widget|
212
+ if container.align == :bottom
213
+ widget.bottom(container.content_bottom, :pt)
214
+ else
215
+ container_full = true
216
+ widget.top(container.content_top, :pt)
217
+ end
218
+ widget.height(widget.preferred_height(writer), :pt) if widget.height.nil?
219
+ end
220
+ left = container.content_left
221
+ right = container.content_right
222
+ lpanels.each do |widget|
223
+ next if widget.disabled
224
+ widget.left(left, :pt)
225
+ left += (widget.width + @style.hpadding)
226
+ end
227
+ rpanels.reverse.each do |widget|
228
+ next if widget.disabled
229
+ widget.right(right, :pt)
230
+ right -= (widget.width + @style.hpadding)
231
+ end
232
+ unaligned.each do |widget|
233
+ next if widget.disabled
234
+ widget.left(left, :pt)
235
+ left += (widget.width + @style.hpadding)
236
+ end
237
+ if container.height.nil?
238
+ content_height = static.map { |widget| widget.height }.max || 0
239
+ container.height(content_height + container.non_content_height, :pt)
240
+ end
241
+ static.each { |widget| widget.layout_widget(writer) if widget.visible and !widget.disabled }
242
+ super(container, writer)
243
+ end
244
+
245
+ def preferred_height(grid, writer)
246
+ cells = grid.row(0)
247
+ return 0 if cells.empty?
248
+ cell_heights = cells.map { |w| w.preferred_height(writer) }
249
+ return nil unless cell_heights.all?
250
+ cell_heights.max
251
+ end
252
+
253
+ def preferred_width(grid, writer)
254
+ cells = grid.row(0)
255
+ return 0 if cells.empty?
256
+ cell_widths = cells.map { |w| w.preferred_width(writer) }
257
+ return nil unless cell_widths.all?
258
+ cell_widths.inject((cells.size - 1) * @style.hpadding) { |sum, width| sum + width }
259
+ end
260
+ end
261
+
262
+ class VBoxLayout < LayoutManager
263
+ register('vbox', self)
264
+
265
+ alias :grid :col_grid
266
+
267
+ def layout(container, writer)
268
+ # $stderr.puts "layout container: #{container.tag}"
269
+ container_full = false
270
+ widgets, remaining = printable_widgets(container, :static)
271
+ remaining.each { |widget| widget.visible = false if widget.printed }
272
+ static, relative = widgets.partition { |widget| widget.position == :static }
273
+ headers, unaligned = static.partition { |widget| widget.align == :top }
274
+ footers, unaligned = unaligned.partition { |widget| widget.align == :bottom }
275
+ static.each do |widget|
276
+ widget.before_layout
277
+ # puts "<1> vbox widget width: #{widget.width} #{widget.path}"
278
+ widget.width([widget.preferred_width(writer) || container.content_width, container.content_width].min, :pt) if widget.width.nil?
279
+ # puts "<2> vbox widget width: #{widget.width} #{widget.path}"
280
+ widget.left(container.content_left, :pt)
281
+ end
282
+ top, dy = container.content_top, 0
283
+ bottom = container.content_top + container.max_content_height
284
+
285
+ headers.each_with_index do |widget, index|
286
+ widget.top(top, :pt)
287
+ widget.layout_widget(writer) # swapped
288
+ widget.height(widget.preferred_height(writer), :pt) if widget.height.nil? # swapped
289
+ top += (widget.height + @style.vpadding)
290
+ dy += widget.height + ((index > 0) ? @style.vpadding : 0)
291
+ end
292
+ headers.each { |widget| widget.visible = (widget.bottom <= bottom) } # or first static widget?
293
+
294
+ unless footers.empty?
295
+ container.height('100%') if container.height.nil?
296
+ footers.reverse.each do |widget|
297
+ widget.bottom(bottom, :pt)
298
+ widget.layout_widget(writer) # swapped
299
+ widget.height(widget.preferred_height(writer), :pt) if widget.height.nil? # swapped
300
+ bottom -= (widget.height + @style.vpadding)
301
+ end
302
+ end
303
+ footers.each { |widget| widget.visible = (widget.top >= top) } # or first static widget?
304
+
305
+ widgets_visible = 0
306
+ unaligned.each_with_index do |widget, index|
307
+ widget.visible = !container_full
308
+ next if container_full
309
+ widget.top(top, :pt)
310
+ # puts "<1> vbox widget height: #{widget.height} #{widget.path}"
311
+ widget.layout_widget(writer) # swapped
312
+ # puts "<2> vbox widget height: #{widget.height} #{widget.path}"
313
+ widget.height(widget.preferred_height(writer), :pt) if widget.height.nil? # swapped
314
+ # puts "<3> vbox widget height: #{widget.height} #{widget.path}"
315
+ top += widget.height
316
+ dy += widget.height + (index > 0 ? @style.vpadding : 0) #if widget.visible
317
+ if top > bottom
318
+ container_full = true
319
+ widget.visible = (widgets_visible == 0)
320
+ # widget.visible = widget.leaves > 0 and container.root_page.positioned_widgets[:static] == 0
321
+ # $stderr.puts "+++vbox+++ #{container.root_page.positioned_widgets[:static]}, tag: #{widget.tag}, visible: #{widget.visible}"
322
+ end
323
+ widgets_visible += 1 if widget.visible
324
+ top += @style.vpadding
325
+ end
326
+ # set_height = container.height.nil?
327
+ # container.height(container.max_height_avail, :pt) if set_height
328
+ # unaligned.each_with_index do |widget, index|
329
+ # # widget.visible = (widget.bottom <= bottom) || (index == 0) #|| (container.overflow && widget.top < bottom)
330
+ # widget.visible = (widget.bottom <= bottom) || (container.root_page.positioned_widgets[:static] == 0) #|| (container.overflow && widget.top < bottom)
331
+ # # if widget.visible and widget.bottom > bottom and container.overflow
332
+ # # widget.layout_widget(writer)
333
+ # # end
334
+ # end
335
+
336
+ container_full = unaligned.last && !unaligned.last.visible
337
+ container.more(true) if container_full and container.overflow
338
+ # container.height(top - container.content_top + @style.vpadding, :pt) if container.height.nil?
339
+ # container.height(dy + container.non_content_height, :pt) if container.height.nil?
340
+ super(container, writer)
341
+ end
342
+
343
+ def preferred_height(grid, writer)
344
+ cells = grid.col(0)
345
+ return 0 if cells.empty?
346
+ cell_heights = cells.map { |w| w.preferred_height(writer) }
347
+ return nil unless cell_heights.all?
348
+ cell_heights.inject((cells.size - 1) * @style.vpadding) { |sum, height| sum + height }
349
+ end
350
+
351
+ def preferred_width(grid, writer)
352
+ cells = grid.col(0)
353
+ return 0 if cells.empty?
354
+ cell_widths = cells.map { |w| w.preferred_width(writer) }
355
+ return nil unless cell_widths.all?
356
+ cell_widths.max
357
+ end
358
+
359
+ # def after_layout(container)
360
+ # container.children.each do |widget|
361
+ # if widget.visible and widget.position == :static
362
+ # if widget.bottom > container.content_bottom
363
+ # widget.disabled = !container.overflow
364
+ # end
365
+ # # widget.after_layout if widget.visible
366
+ # end
367
+ # end
368
+ # end
369
+ end
370
+
371
+ class TableLayout < LayoutManager
372
+ register('table', self)
373
+
374
+ private
375
+ ROW_SPAN = 0
376
+ COL_SPAN = 0
377
+ ROW_HEIGHT = 1
378
+ COL_WIDTH = 1
379
+ def mark_grid(grid, a, b, c, d, value)
380
+ c.times do |aa|
381
+ d.times do |bb|
382
+ grid[a + aa, b + bb] = value if aa > 0 or bb > 0
383
+ end
384
+ end
385
+ end
386
+
387
+ def row_grid(container)
388
+ raise ArgumentError, "cols must be specified." if container.cols.nil?
389
+ static = printable_widgets(container, :static).first
390
+ grid = Support::Grid.new(container.cols, 0)
391
+ row = col = 0
392
+ static.each do |widget|
393
+ while grid[col, row] == false
394
+ col += 1
395
+ if col >= container.cols then row += 1; col = 0 end
396
+ end
397
+ grid[col, row] = widget
398
+ mark_grid(grid, col, row, widget.colspan, widget.rowspan, false)
399
+ col += widget.colspan
400
+ raise ArgumentError, "colspan causes number of columns to exceed table size." if col > container.cols
401
+ if col == container.cols then row += 1; col = 0 end
402
+ end
403
+ grid
404
+ end
405
+
406
+ def col_grid(container)
407
+ raise ArgumentError, "rows must be specified." if container.rows.nil?
408
+ static = printable_widgets(container, :static).first
409
+ grid = Support::Grid.new(0, container.rows)
410
+ row = col = 0
411
+ static.each do |widget|
412
+ while grid[col, row] == false
413
+ row += 1
414
+ if row >= container.rows then col += 1; row = 0 end
415
+ end
416
+ if row >= container.rows then col += 1; row = 0 end
417
+ grid[col, row] = widget
418
+ mark_grid(grid, col, row, widget.colspan, widget.rowspan, false)
419
+ row += widget.rowspan
420
+ raise ArgumentError, "rowspan causes number of rows to exceed table size." if row > container.rows
421
+ end
422
+ grid
423
+ end
424
+
425
+ def detect_widths(grid, writer)
426
+ widths = []
427
+ grid.cols.times do |c|
428
+ col = grid.col(c)
429
+ widget = col.detect { |w| w and (w.colspan == 1) }
430
+ if widget.nil?
431
+ widths << [:unspecified, 0]
432
+ elsif widget.width_pct
433
+ widths << [:percent, widget.width]
434
+ elsif widget.width
435
+ widths << [:specified, widget.width]
436
+ else
437
+ widths << [:unspecified, col.map { |w| w ? w.preferred_width(writer) : 0 }.max]
438
+ end
439
+ end
440
+ widths
441
+ end
442
+
443
+ def allocate_specified_widths(width_avail, specified)
444
+ specified.each do |w|
445
+ if width_avail < w[COL_WIDTH]
446
+ # w[COL_WIDTH] = 0
447
+ else
448
+ width_avail -= (w[COL_WIDTH] + @style.hpadding)
449
+ end
450
+ end
451
+ width_avail
452
+ end
453
+
454
+ def allocate_percent_widths(width_avail, percents)
455
+ # allocate percent widths with a minimum width of 1 point
456
+ if width_avail - (percents.size - 1) * @style.hpadding >= percents.size
457
+ width_avail -= (percents.size - 1) * @style.hpadding
458
+ total_percents = percents.inject(0) { |total, w| total + w[COL_WIDTH] }
459
+ ratio = width_avail.quo(total_percents)
460
+ percents.each do |w|
461
+ w[COL_WIDTH] = w[COL_WIDTH] * ratio if ratio < 1.0
462
+ width_avail -= w[COL_WIDTH]
463
+ end
464
+ else
465
+ percents.each { |w| w[COL_WIDTH] = 0 }
466
+ end
467
+ width_avail -= @style.hpadding
468
+ width_avail
469
+ end
470
+
471
+ def allocate_other_widths(width_avail, others)
472
+ # divide remaining width equally among widgets with unspecified widths
473
+ if width_avail - (others.size - 1) * @style.hpadding >= others.size
474
+ width_avail -= (others.size - 1) * @style.hpadding
475
+ others_width = width_avail.quo(others.size)
476
+ others.each { |w| w[COL_WIDTH] = others_width }
477
+ else
478
+ others.each { |w| w[COL_WIDTH] = 0 }
479
+ end
480
+ width_avail
481
+ end
482
+
483
+ def layout_grid(grid, container, writer)
484
+ container_full = false
485
+ widths = detect_widths(grid, writer)
486
+ if container.width.nil?
487
+ puts "Noooooooo!!!!"
488
+ end
489
+ percents, others = widths.partition { |w| w[0] == :percent }
490
+ specified, others = others.partition { |w| w[0] == :specified }
491
+
492
+ width_avail = container.content_width
493
+ width_avail = allocate_specified_widths(width_avail, specified)
494
+ width_avail = allocate_percent_widths(width_avail, percents)
495
+ width_avail = allocate_other_widths(width_avail, others)
496
+
497
+ heights = Support::Grid.new(grid.cols, grid.rows)
498
+ grid.cols.times do |c|
499
+ grid.col(c).each_with_index do |widget, r|
500
+ next unless widget
501
+ if widths[c][COL_WIDTH] > 0
502
+ width = (0...widget.colspan).inject(0) { |width, i| width + widths[c + i][COL_WIDTH] }
503
+ widget.width(width + (widget.colspan - 1) * @style.hpadding, :pt)
504
+ else
505
+ # widget.visible = false
506
+ widget.disabled = true
507
+ next
508
+ end
509
+ heights[c, r] = [widget.rowspan, widget.height || widget.preferred_height(writer)]
510
+ end
511
+ end
512
+
513
+ heights.rows.times do |r|
514
+ row_heights = (0...heights.cols).map { |c| heights[c,r] }.compact
515
+ min_rowspan = row_heights.map { |rowspan, height| rowspan }.min
516
+ min_rowspan_heights = row_heights.select { |rowspan, height| rowspan == min_rowspan }
517
+ max_height = min_rowspan_heights.map { |rowspan, height| height }.max
518
+ heights.cols.times do |c|
519
+ rh = heights[c,r]
520
+ next if rh.nil?
521
+ if rh[ROW_SPAN] > min_rowspan
522
+ heights[c,r+1] = [rh[ROW_SPAN] - 1, [rh[ROW_HEIGHT] - max_height, 0].max]
523
+ end
524
+ rh[ROW_HEIGHT] = max_height
525
+ end
526
+ end
527
+
528
+ top = container.content_top
529
+ bottom = container.content_top + container.max_content_height
530
+ grid.rows.times do |r|
531
+ max_height = 0
532
+ left = container.content_left
533
+ grid.cols.times do |c|
534
+ if widget = grid[c, r]
535
+ widget.visible = !container_full
536
+ next if container_full
537
+ rh = heights[c,r]
538
+ next if rh.nil?
539
+ widget.top(top, :pt)
540
+ widget.left(left, :pt)
541
+ height = (0...rh[ROW_SPAN]).inject((rh[ROW_SPAN] - 1) * @style.vpadding) { |height, row_offset| height + heights[c,r+row_offset][ROW_HEIGHT] }
542
+ widget.height(height, :pt)
543
+ max_height = [max_height, rh[ROW_HEIGHT]].max if rh[ROW_SPAN] == 1
544
+ end
545
+ left += widths[c][1] + @style.hpadding
546
+ end
547
+ next if container_full
548
+ if top + max_height > bottom
549
+ container_full = true
550
+ grid.cols.times { |c| grid[c, r].visible = (r == 0) if grid[c, r] }
551
+ container.more(true) if container.overflow and (r > 0)
552
+ end
553
+ top += max_height + @style.vpadding unless container_full
554
+ end
555
+ if container.height.nil?
556
+ container.height(top - container.content_top + container.non_content_height - @style.vpadding, :pt)
557
+ end
558
+ static, remaining = printable_widgets(container, :static)
559
+ remaining.each { |widget| widget.visible = false if widget.printed }
560
+ static.each { |widget| widget.layout_widget(writer) }
561
+ end
562
+
563
+ public
564
+ def grid(container)
565
+ if container.order == :rows
566
+ row_grid(container)
567
+ else # container.order == :cols
568
+ col_grid(container)
569
+ end
570
+ end
571
+
572
+ def layout(container, writer)
573
+ layout_grid(grid(container), container, writer)
574
+ super(container, writer)
575
+ end
576
+
577
+ def preferred_height(grid, writer)
578
+ # calculate preferred heights, where available
579
+ heights = Support::Grid.new(grid.cols, grid.rows)
580
+ return 0 if heights.cols == 0 or heights.rows == 0
581
+ grid.cols.times do |c|
582
+ grid.col(c).each_with_index do |widget, r|
583
+ next unless widget
584
+ # heights[c, r] = [widget.rowspan, widget.has_height? ? widget.preferred_height(writer) : nil]
585
+ heights[c, r] = [widget.rowspan, widget.preferred_height(writer)]
586
+ end
587
+ end
588
+
589
+ heights.rows.times do |r|
590
+ row_heights = (0...heights.cols).map { |c| heights[c,r] }.compact
591
+ min_rowspan = row_heights.map { |rowspan, height| rowspan }.min
592
+ min_rowspan_heights = row_heights.select { |rowspan, height| rowspan == min_rowspan }
593
+ max_height = min_rowspan_heights.map { |rowspan, height| height }.compact.max
594
+ # at least one cell must specify a height
595
+ return nil if max_height.nil?
596
+ heights.cols.times do |c|
597
+ rh = heights[c,r]
598
+ next if rh.nil?
599
+ # carry height in excess of max height of cells with min_rowspan to cell in next row, subtracting vpadding
600
+ if rh[ROW_SPAN] > min_rowspan
601
+ heights[c,r+1] = [rh[ROW_SPAN] - 1, [rh[ROW_HEIGHT] - max_height - @style.vpadding, 0].max]
602
+ end
603
+ rh[ROW_HEIGHT] = max_height
604
+ end
605
+ end
606
+
607
+ result = 0
608
+ grid.rows.times do |r|
609
+ max_height = 0
610
+ grid.cols.times do |c|
611
+ if (widget = grid[c, r]) and (rh = heights[c,r])
612
+ height = (0...rh[ROW_SPAN]).inject((rh[ROW_SPAN] - 1) * @style.vpadding) { |height, row_offset| height + heights[c,r+row_offset][ROW_HEIGHT] }
613
+ max_height = [max_height, rh[ROW_HEIGHT]].max if rh[ROW_SPAN] == 1
614
+ end
615
+ end
616
+ result += max_height + @style.vpadding
617
+ end
618
+ result -= @style.vpadding if result > 0
619
+ end
620
+
621
+ def preferred_width(grid, writer)
622
+ # calculate preferred widths, where available
623
+ widths = Support::Grid.new(grid.cols, grid.rows)
624
+ return 0 if widths.cols == 0 or widths.rows == 0
625
+ grid.rows.times do |r|
626
+ grid.row(r).each_with_index do |widget, c|
627
+ next unless widget
628
+ # widths[c, r] = [widget.colspan, widget.has_width? ? widget.preferred_width(writer) : nil]
629
+ widths[c, r] = [widget.colspan, widget.preferred_width(writer)]
630
+ end
631
+ end
632
+
633
+ widths.cols.times do |c|
634
+ col_widths = (0...widths.rows).map { |r| widths[c,r] }.compact
635
+ min_colspan = col_widths.map { |colspan, width| colspan }.min
636
+ min_colspan_widths = col_widths.select { |colspan, width| colspan == min_colspan }
637
+ max_width = min_colspan_widths.map { |colspan, width| width }.compact.max
638
+ # at least one cell must specify a width
639
+ return nil if max_width.nil?
640
+ widths.rows.times do |r|
641
+ cw = widths[c,r]
642
+ next if cw.nil?
643
+ # carry width in excess of max width of cells with min_colspan to cell in next col, subtracting hpadding
644
+ if cw[COL_SPAN] > min_colspan
645
+ widths[c+1,r] = [cw[COL_SPAN] - 1, [cw[COL_WIDTH] - max_width - @style.hpadding, 0].max]
646
+ end
647
+ cw[COL_WIDTH] = max_width
648
+ end
649
+ end
650
+
651
+ result = 0
652
+ grid.cols.times do |c|
653
+ max_width = 0
654
+ grid.rows.times do |r|
655
+ if (widget = grid[c, r]) and (cw = widths[c, r])
656
+ width = (0...cw[COL_SPAN]).inject((cw[COL_SPAN] - 1) * @style.hpadding) { |width, col_offset| width + widths[c+col_offset,r][COL_WIDTH] }
657
+ max_width = [max_width, cw[COL_WIDTH]].max if cw[COL_SPAN] == 1
658
+ end
659
+ end
660
+ result += max_width + @style.hpadding
661
+ end
662
+ result -= @style.hpadding if result > 0
663
+ result
664
+ end
665
+ end
666
+ end
667
+ end