eideticrml 0.3.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.
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