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.
- checksums.yaml +7 -0
- data/Rakefile +55 -0
- data/lib/erml.rb +345 -0
- data/lib/erml_layout_managers.rb +667 -0
- data/lib/erml_rules.rb +104 -0
- data/lib/erml_styles.rb +304 -0
- data/lib/erml_support.rb +105 -0
- data/lib/erml_widget_factories.rb +38 -0
- data/lib/erml_widgets.rb +1895 -0
- data/samples/test10_rich_text.erml +17 -0
- data/samples/test11_table_layout.erml +30 -0
- data/samples/test12_shapes.erml +32 -0
- data/samples/test13_polygons.erml +28 -0
- data/samples/test14_images.erml +19 -0
- data/samples/test15_lines.erml +43 -0
- data/samples/test16_classes.erml +34 -0
- data/samples/test17_rules.erml +24 -0
- data/samples/test18_preformatted_text.erml +9 -0
- data/samples/test19_erb.erml.erb +26 -0
- data/samples/test1_empty_doc.erml +2 -0
- data/samples/test20_haml.erml.haml +20 -0
- data/samples/test21_shift_widgets.erml +47 -0
- data/samples/test22_multipage_flow_layout.erml +40 -0
- data/samples/test23_pageno.erml +17 -0
- data/samples/test24_headers_footers.erml.erb +37 -0
- data/samples/test25_overflow.erml.erb +37 -0
- data/samples/test26_columns.erml.erb +42 -0
- data/samples/test28_landscape.erml.erb +17 -0
- data/samples/test29_pages_up.erml.erb +17 -0
- data/samples/test2_empty_page.erml +6 -0
- data/samples/test30_encodings.erml.haml +35 -0
- data/samples/test3_hello_world.erml +7 -0
- data/samples/test4_two_pages.erml +10 -0
- data/samples/test5_rounded_rect.erml +10 -0
- data/samples/test6_bullets.erml +16 -0
- data/samples/test7_flow_layout.erml +20 -0
- data/samples/test8_vbox_layout.erml +23 -0
- data/samples/test9_hbox_layout.erml +22 -0
- data/samples/testimg.jpg +0 -0
- data/test/test_erml_layout_managers.rb +106 -0
- data/test/test_erml_rules.rb +116 -0
- data/test/test_erml_styles.rb +415 -0
- data/test/test_erml_support.rb +140 -0
- data/test/test_erml_widget_factories.rb +46 -0
- data/test/test_erml_widgets.rb +1235 -0
- data/test/test_helpers.rb +18 -0
- metadata +102 -0
data/lib/erml_rules.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brent Rowland on 2008-02-06.
|
4
|
+
# Copyright (c) 2008 Eidetic Software. All rights reserved.
|
5
|
+
|
6
|
+
module EideticRML
|
7
|
+
module Rules
|
8
|
+
class Rule
|
9
|
+
attr_reader :parent
|
10
|
+
attr_reader :selector
|
11
|
+
attr_reader :attrs
|
12
|
+
|
13
|
+
def initialize(rules, selector, attrs={}, parent=nil)
|
14
|
+
@rules, @parent = rules, parent
|
15
|
+
@selector = parent.nil? ? selector : parent.selector.dup << ' ' << selector
|
16
|
+
@attrs = attrs.dup
|
17
|
+
end
|
18
|
+
|
19
|
+
def root
|
20
|
+
parent.nil? ? self : parent.root
|
21
|
+
end
|
22
|
+
|
23
|
+
def update(attrs)
|
24
|
+
@attrs.update(attrs)
|
25
|
+
@selector_re = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def ===(path)
|
29
|
+
selector_re =~ path
|
30
|
+
end
|
31
|
+
|
32
|
+
GT_RE = '\\/'
|
33
|
+
SPACE_RE = GT_RE + '([^\\/]+\\/)*'
|
34
|
+
TAG_RE = '\\w+'
|
35
|
+
ID_RE = '(#\\w+)?'
|
36
|
+
MISC_CLASS_RE = '(\\.\\w+)*'
|
37
|
+
SPEC_CLASS_RE = '(\\.\\w+)*\\.%s(\\.\\w+)*'
|
38
|
+
|
39
|
+
def selector_re
|
40
|
+
@selector_re ||= Regexp.compile(Rule.selector_re_s(@selector))
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.selector_re_s(selector)
|
44
|
+
selectors = selector.split(',').map { |sel| sel.strip }
|
45
|
+
result = selectors.map do |sel|
|
46
|
+
sel.split(' ').map { |group| group_re_s(group) }.join(SPACE_RE) << '$'
|
47
|
+
end
|
48
|
+
result.size > 1 ? '(' << result.join('|') << ')' : result.first
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.group_re_s(group)
|
52
|
+
group.split('>').map { |item| item_re_s(item) }.join(GT_RE)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.item_re_s(item)
|
56
|
+
t, k = item.split('.', 2)
|
57
|
+
if t == ''
|
58
|
+
t = TAG_RE + ID_RE
|
59
|
+
elsif t[0] == ?#
|
60
|
+
t = TAG_RE + t
|
61
|
+
elsif !t.include?('#')
|
62
|
+
t << ID_RE
|
63
|
+
end
|
64
|
+
if k.nil? or k.empty?
|
65
|
+
k = MISC_CLASS_RE
|
66
|
+
else
|
67
|
+
k = SPEC_CLASS_RE % k
|
68
|
+
end
|
69
|
+
t + k
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.parse(text)
|
73
|
+
text.scan(/\s*([^\{]+)\s*\{([^\}]+)\}/).map do |selector, rule|
|
74
|
+
selector.strip!
|
75
|
+
selector.gsub!(/\s{2,}/,' ') # collapse whitespace
|
76
|
+
selector.gsub!(/\s*>\s*/,'>') # strip whitespace from right angle brackets
|
77
|
+
attrs = rule.scan(/\s*([^:]+)\s*:\s*([^;]+)\s*;?/).inject({}) { |attrs, (k, v)| attrs[k.gsub('-', '_')] = v.strip; attrs }
|
78
|
+
[selector, attrs]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class RuleCollection < Array
|
84
|
+
def add(selector, attrs={})
|
85
|
+
rule = for_selector(selector)
|
86
|
+
if rule.nil?
|
87
|
+
rule = Rule.new(self, selector, attrs)
|
88
|
+
self << rule
|
89
|
+
else
|
90
|
+
rule.update(attrs)
|
91
|
+
end
|
92
|
+
rule
|
93
|
+
end
|
94
|
+
|
95
|
+
def for_selector(selector)
|
96
|
+
find { |rule| rule.selector == selector }
|
97
|
+
end
|
98
|
+
|
99
|
+
def matching(path)
|
100
|
+
select { |rule| rule === path }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/erml_styles.rb
ADDED
@@ -0,0 +1,304 @@
|
|
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
|
+
require 'erml_layout_managers'
|
8
|
+
|
9
|
+
module EideticRML
|
10
|
+
module Styles
|
11
|
+
module HasColor
|
12
|
+
def color(value=nil)
|
13
|
+
return @color || 0 if value.nil?
|
14
|
+
@color = case value
|
15
|
+
when /^#([0-9A-Fa-f]{6})$/ # 6-digit hex color
|
16
|
+
$1.to_i(16)
|
17
|
+
when /^#([0-9A-Fa-f]{3})$/ # 3-digit hex color
|
18
|
+
$1.gsub(/(\w)/, '\\1\\1').to_i(16)
|
19
|
+
else
|
20
|
+
value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module HasWidth
|
26
|
+
def width(value=nil, units=nil)
|
27
|
+
return @width || 0 if value.nil?
|
28
|
+
return from_points(@width, value) if value.is_a?(Symbol)
|
29
|
+
@width = parse_measurement_pts(value, units || self.units)
|
30
|
+
end
|
31
|
+
|
32
|
+
def units(value=nil)
|
33
|
+
return @units || :pt if value.nil?
|
34
|
+
@units = value.to_sym
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Style
|
39
|
+
include Support
|
40
|
+
|
41
|
+
def initialize(styles, attrs={})
|
42
|
+
@styles = styles
|
43
|
+
attrs.each { |key, value| self.send(key, value) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize_copy(other)
|
47
|
+
@id = nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def from_points(value, units)
|
51
|
+
value.to_f / EideticPDF::UNIT_CONVERSION[units]
|
52
|
+
end
|
53
|
+
|
54
|
+
def id(value=nil)
|
55
|
+
return @id if value.nil?
|
56
|
+
@id = value.to_s
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.register(name, klass)
|
60
|
+
(@@klasses ||= {})[name] = klass
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.for_name(name)
|
64
|
+
@@klasses[name] unless @@klasses.nil?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class StyleCollection < Array
|
69
|
+
def add(name, attrs={})
|
70
|
+
raise ArgumentError, "id required for style" if attrs[:id].nil?
|
71
|
+
style = for_id(attrs[:id])
|
72
|
+
if style.nil?
|
73
|
+
if (style_class = Style.for_name(name.to_s)).nil?
|
74
|
+
raise ArgumentError, "Unknown style #{name}."
|
75
|
+
end
|
76
|
+
style = style_class.new(self, attrs)
|
77
|
+
self << style
|
78
|
+
else
|
79
|
+
attrs.each { |key, value| style.send(key, value) }
|
80
|
+
end
|
81
|
+
style
|
82
|
+
end
|
83
|
+
|
84
|
+
def for_id(id)
|
85
|
+
find { |style| style.id == id }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class PenStyle < Style
|
90
|
+
register('pen', self)
|
91
|
+
|
92
|
+
include HasColor
|
93
|
+
include HasWidth
|
94
|
+
|
95
|
+
def apply(writer)
|
96
|
+
writer.line_color(color)
|
97
|
+
writer.line_width([width, 0.001].max, units)
|
98
|
+
writer.line_dash_pattern(pattern)
|
99
|
+
writer.line_cap_style(cap)
|
100
|
+
end
|
101
|
+
|
102
|
+
def cap(value=nil)
|
103
|
+
return @cap || :butt_cap if value.nil?
|
104
|
+
@cap = value.to_sym if EideticPDF::LINE_CAP_STYLES.include?(value.to_sym)
|
105
|
+
end
|
106
|
+
|
107
|
+
def pattern(value=nil)
|
108
|
+
return @pattern || :solid if value.nil?
|
109
|
+
@pattern = EideticPDF::LINE_PATTERNS[value.to_sym] ? value.to_sym : value.to_s
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class BrushStyle < Style
|
114
|
+
register('brush', self)
|
115
|
+
|
116
|
+
include HasColor
|
117
|
+
|
118
|
+
def apply(writer)
|
119
|
+
writer.fill_color(color)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class FontStyle < Style
|
124
|
+
register('font', self)
|
125
|
+
|
126
|
+
include HasColor
|
127
|
+
|
128
|
+
def apply(writer)
|
129
|
+
writer.line_height(line_height)
|
130
|
+
writer.font(name, size, :style => style, :weight => weight, :color => color, :encoding => encoding, :sub_type => sub_type)
|
131
|
+
end
|
132
|
+
|
133
|
+
def name(value=nil)
|
134
|
+
return @name || EideticPDF::PageWriter::DEFAULT_FONT[:name] if value.nil?
|
135
|
+
@name = value
|
136
|
+
end
|
137
|
+
|
138
|
+
def size(value=nil)
|
139
|
+
return @size || EideticPDF::PageWriter::DEFAULT_FONT[:size] if value.nil?
|
140
|
+
@size = value.to_f
|
141
|
+
end
|
142
|
+
|
143
|
+
def strikeout(value=nil)
|
144
|
+
return @strikeout if value.nil?
|
145
|
+
@strikeout = (value == true) || (value == 'true')
|
146
|
+
end
|
147
|
+
|
148
|
+
def style(value=nil)
|
149
|
+
return @style || '' if value.nil?
|
150
|
+
@style = value
|
151
|
+
end
|
152
|
+
|
153
|
+
def encoding(value=nil)
|
154
|
+
return @encoding || 'WinAnsiEncoding' if value.nil?
|
155
|
+
@encoding = value
|
156
|
+
end
|
157
|
+
|
158
|
+
def sub_type(value=nil)
|
159
|
+
return @sub_type || 'Type1' if value.nil?
|
160
|
+
@sub_type = value
|
161
|
+
end
|
162
|
+
|
163
|
+
def underline(value=nil)
|
164
|
+
return @underline if value.nil?
|
165
|
+
@underline = (value == true) || (value == 'true')
|
166
|
+
end
|
167
|
+
|
168
|
+
def weight(value=nil)
|
169
|
+
return @weight if value.nil?
|
170
|
+
@weight = value
|
171
|
+
end
|
172
|
+
|
173
|
+
def line_height(value=nil)
|
174
|
+
return @line_height || 1.7 if value.nil?
|
175
|
+
@line_height = value.to_f
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
class BulletStyle < Style
|
180
|
+
register('bullet', self)
|
181
|
+
|
182
|
+
include HasWidth
|
183
|
+
|
184
|
+
def initialize(styles, attrs={})
|
185
|
+
@width, @units = 36, :pt
|
186
|
+
super(styles, attrs)
|
187
|
+
end
|
188
|
+
|
189
|
+
def apply(writer)
|
190
|
+
unless writer.bullet(id)
|
191
|
+
writer.bullet(id, :width => width) do |w|
|
192
|
+
prev_font = w.font(@font.name, @font.size, :style => @font.style, :encoding => @font.encoding, :sub_type => @font.sub_type)
|
193
|
+
w.print(text)
|
194
|
+
w.font(prev_font)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def font(value=nil)
|
200
|
+
return @font if value.nil?
|
201
|
+
f = @styles.for_id(value)
|
202
|
+
raise ArgumentError, "Font Style #{value} not found." unless f.is_a?(Styles::FontStyle)
|
203
|
+
@font = f
|
204
|
+
end
|
205
|
+
|
206
|
+
def text(value=nil)
|
207
|
+
return @text if value.nil?
|
208
|
+
@text = value
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
class TextStyle < Style
|
213
|
+
include HasColor
|
214
|
+
|
215
|
+
def text_align(value=nil)
|
216
|
+
return @text_align || :left if value.nil?
|
217
|
+
@text_align = [:left, :center, :right, :justify].include?(value.to_sym) ? value.to_sym : @text_align
|
218
|
+
end
|
219
|
+
|
220
|
+
def apply(writer)
|
221
|
+
writer.font_color(color)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
class LabelStyle < TextStyle
|
226
|
+
register('label', self)
|
227
|
+
|
228
|
+
def angle(value=nil)
|
229
|
+
return @angle || 0 if value.nil?
|
230
|
+
@angle = value.to_f
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
class ParagraphStyle < TextStyle
|
235
|
+
register('para', self)
|
236
|
+
|
237
|
+
def apply(writer)
|
238
|
+
super(writer)
|
239
|
+
@bullet.apply(writer) unless @bullet.nil?
|
240
|
+
end
|
241
|
+
|
242
|
+
def bullet(value=nil)
|
243
|
+
return @bullet if value.nil?
|
244
|
+
b = @styles.for_id(value)
|
245
|
+
raise ArgumentError, "Bullet Style #{value} not found." unless b.is_a?(Styles::BulletStyle)
|
246
|
+
@bullet = b
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
class PageStyle < Style
|
251
|
+
register('page', self)
|
252
|
+
|
253
|
+
def height(units=:pt)
|
254
|
+
from_points(EideticPDF::PageStyle::SIZES[size][orientation == :portrait ? 1 : 0], units)
|
255
|
+
end
|
256
|
+
|
257
|
+
def orientation(value=nil)
|
258
|
+
return @orientation || :portrait if value.nil?
|
259
|
+
@orientation = value.to_sym if [:portrait, :landscape].include?(value.to_sym)
|
260
|
+
end
|
261
|
+
|
262
|
+
def size(value=nil)
|
263
|
+
return @size || :letter if value.nil?
|
264
|
+
@size = value.to_sym if EideticPDF::PageStyle::SIZES[value.to_sym]
|
265
|
+
end
|
266
|
+
|
267
|
+
def width(units=:pt)
|
268
|
+
from_points(EideticPDF::PageStyle::SIZES[size][orientation == :portrait ? 0 : 1], units)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
class LayoutStyle < Style
|
273
|
+
register('layout', self)
|
274
|
+
|
275
|
+
def padding(value=nil, units=:pt)
|
276
|
+
return @padding || 0 if value.nil?
|
277
|
+
return from_points(@padding || 0, value) if value.is_a?(Symbol)
|
278
|
+
@padding = parse_measurement_pts(value, units)
|
279
|
+
end
|
280
|
+
|
281
|
+
def hpadding(value=nil, units=:pt)
|
282
|
+
return @hpadding || padding if value.nil?
|
283
|
+
return from_points(@hpadding || padding, value) if value.is_a?(Symbol)
|
284
|
+
@hpadding = parse_measurement_pts(value, units)
|
285
|
+
end
|
286
|
+
|
287
|
+
def vpadding(value=nil, units=:pt)
|
288
|
+
return @vpadding || padding if value.nil?
|
289
|
+
return from_points(@vpadding || padding, value) if value.is_a?(Symbol)
|
290
|
+
@vpadding = parse_measurement_pts(value, units)
|
291
|
+
end
|
292
|
+
|
293
|
+
def units(value=nil)
|
294
|
+
return @units || :pt if value.nil?
|
295
|
+
@units = value.to_sym if EideticPDF::UNIT_CONVERSION[value.to_sym]
|
296
|
+
end
|
297
|
+
|
298
|
+
def manager(value=nil)
|
299
|
+
return @manager if value.nil?
|
300
|
+
@manager = LayoutManagers::LayoutManager.for_name(value).new(self)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
data/lib/erml_support.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brent Rowland on 2008-01-07.
|
4
|
+
# Copyright (c) 2008 Eidetic Software. All rights reserved.
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
if File.exist?(File.expand_path(File.join('..', 'eideticpdf')))
|
8
|
+
$:.unshift(File.expand_path(File.join('..', 'eideticpdf', 'lib')))
|
9
|
+
else
|
10
|
+
gem 'eideticpdf'
|
11
|
+
end
|
12
|
+
require 'epdfdw'
|
13
|
+
|
14
|
+
module EideticRML
|
15
|
+
module Support
|
16
|
+
def parse_measurement(value, units=:pt)
|
17
|
+
value, units = if value =~ /([+-]?\d+(\.\d+)?)([a-z]+)/
|
18
|
+
[$1.to_f, ($3 || :pt).to_sym]
|
19
|
+
else
|
20
|
+
[value.to_f, units.to_sym]
|
21
|
+
end
|
22
|
+
units = :pt unless EideticPDF::UNIT_CONVERSION[units]
|
23
|
+
[value, units]
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_measurement_pts(value, units=:pt)
|
27
|
+
v, u = parse_measurement(value, units)
|
28
|
+
v * EideticPDF::UNIT_CONVERSION[u]
|
29
|
+
end
|
30
|
+
|
31
|
+
def from_units(units, measurement)
|
32
|
+
measurement.to_f * EideticPDF::UNIT_CONVERSION[units]
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_units(units, measurement)
|
36
|
+
measurement.to_f / EideticPDF::UNIT_CONVERSION[units]
|
37
|
+
end
|
38
|
+
|
39
|
+
module_function :parse_measurement, :parse_measurement_pts
|
40
|
+
|
41
|
+
class Bounds
|
42
|
+
attr_accessor :left, :top, :right, :bottom
|
43
|
+
|
44
|
+
def initialize(*args)
|
45
|
+
@left, @top, @right, @bottom = *args
|
46
|
+
end
|
47
|
+
|
48
|
+
def width
|
49
|
+
right - left
|
50
|
+
end
|
51
|
+
|
52
|
+
def height
|
53
|
+
bottom - top
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Grid
|
58
|
+
attr_reader :cols
|
59
|
+
attr_accessor :rows
|
60
|
+
|
61
|
+
def initialize(cols, rows)
|
62
|
+
@cols, @rows = cols, rows
|
63
|
+
@cells = Array.new(cols * rows)
|
64
|
+
end
|
65
|
+
|
66
|
+
def cols=(new_cols)
|
67
|
+
@new_cells = Array.new(new_cols * rows)
|
68
|
+
rows.times do |r|
|
69
|
+
@new_cells[r * new_cols, cols] = @cells[r * cols, cols]
|
70
|
+
end
|
71
|
+
@cells = @new_cells
|
72
|
+
@cols = new_cols
|
73
|
+
end
|
74
|
+
|
75
|
+
def rows=(value)
|
76
|
+
if value > rows
|
77
|
+
@cells.fill(nil, cols * rows, cols * (value - rows))
|
78
|
+
elsif value < rows
|
79
|
+
@cells.slice!(cols * value, cols * (rows - value))
|
80
|
+
end
|
81
|
+
@rows = value
|
82
|
+
end
|
83
|
+
|
84
|
+
def [](col, row)
|
85
|
+
@cells[row * cols + col]
|
86
|
+
end
|
87
|
+
|
88
|
+
def []=(col, row, value)
|
89
|
+
self.rows = row + 1 if row >= rows
|
90
|
+
self.cols = col + 1 if col >= cols
|
91
|
+
@cells[row * cols + col] = value
|
92
|
+
end
|
93
|
+
|
94
|
+
def col(index)
|
95
|
+
result = []
|
96
|
+
rows.times { |row| result << self[index, row] }
|
97
|
+
result
|
98
|
+
end
|
99
|
+
|
100
|
+
def row(index)
|
101
|
+
@cells.slice(index * cols, cols)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|