prawn-svg 0.23.1 → 0.24.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 +4 -4
- data/.travis.yml +3 -3
- data/README.md +15 -9
- data/lib/prawn-svg.rb +3 -0
- data/lib/prawn/svg/attributes.rb +1 -1
- data/lib/prawn/svg/attributes/clip_path.rb +6 -7
- data/lib/prawn/svg/attributes/opacity.rb +3 -3
- data/lib/prawn/svg/attributes/stroke.rb +6 -4
- data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +114 -0
- data/lib/prawn/svg/elements.rb +4 -1
- data/lib/prawn/svg/elements/base.rb +76 -69
- data/lib/prawn/svg/elements/container.rb +5 -6
- data/lib/prawn/svg/elements/gradient.rb +4 -4
- data/lib/prawn/svg/elements/image.rb +1 -1
- data/lib/prawn/svg/elements/line.rb +15 -7
- data/lib/prawn/svg/elements/marker.rb +72 -0
- data/lib/prawn/svg/elements/path.rb +23 -147
- data/lib/prawn/svg/elements/polygon.rb +14 -6
- data/lib/prawn/svg/elements/polyline.rb +12 -11
- data/lib/prawn/svg/elements/root.rb +3 -1
- data/lib/prawn/svg/elements/text.rb +38 -17
- data/lib/prawn/svg/font.rb +6 -6
- data/lib/prawn/svg/interface.rb +3 -0
- data/lib/prawn/svg/pathable.rb +130 -0
- data/lib/prawn/svg/properties.rb +122 -0
- data/lib/prawn/svg/state.rb +7 -29
- data/lib/prawn/svg/version.rb +1 -1
- data/spec/prawn/svg/elements/base_spec.rb +19 -32
- data/spec/prawn/svg/elements/line_spec.rb +37 -0
- data/spec/prawn/svg/elements/marker_spec.rb +90 -0
- data/spec/prawn/svg/elements/path_spec.rb +10 -10
- data/spec/prawn/svg/elements/polygon_spec.rb +49 -0
- data/spec/prawn/svg/elements/polyline_spec.rb +47 -0
- data/spec/prawn/svg/elements/style_spec.rb +1 -1
- data/spec/prawn/svg/elements/text_spec.rb +37 -5
- data/spec/prawn/svg/pathable_spec.rb +92 -0
- data/spec/prawn/svg/properties_spec.rb +186 -0
- data/spec/sample_svg/arrows.svg +73 -0
- data/spec/sample_svg/marker.svg +32 -0
- data/spec/sample_svg/polygon01.svg +25 -5
- metadata +23 -8
- data/lib/prawn/svg/attributes/color.rb +0 -5
- data/lib/prawn/svg/attributes/display.rb +0 -5
- data/lib/prawn/svg/attributes/font.rb +0 -38
- data/spec/prawn/svg/attributes/font_spec.rb +0 -52
@@ -1,22 +1,23 @@
|
|
1
1
|
class Prawn::SVG::Elements::Polyline < Prawn::SVG::Elements::Base
|
2
|
+
include Prawn::SVG::Pathable
|
3
|
+
|
2
4
|
def parse
|
3
5
|
require_attributes('points')
|
4
6
|
@points = parse_points(attributes['points'])
|
5
7
|
end
|
6
8
|
|
7
9
|
def apply
|
8
|
-
|
9
|
-
|
10
|
-
add_call 'move_to', *@points[0]
|
11
|
-
add_call_and_enter 'stroke'
|
12
|
-
@points[1..-1].each do |x, y|
|
13
|
-
add_call "line_to", x, y
|
14
|
-
end
|
10
|
+
apply_commands
|
11
|
+
apply_markers
|
15
12
|
end
|
16
13
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
14
|
+
protected
|
15
|
+
|
16
|
+
def commands
|
17
|
+
@commands ||= [
|
18
|
+
Prawn::SVG::Pathable::Move.new(@points[0])
|
19
|
+
] + @points[1..-1].map { |point|
|
20
|
+
Prawn::SVG::Pathable::Line.new(point)
|
21
|
+
}
|
21
22
|
end
|
22
23
|
end
|
@@ -7,7 +7,9 @@ class Prawn::SVG::Elements::Root < Prawn::SVG::Elements::Base
|
|
7
7
|
add_call 'fill_color', '000000'
|
8
8
|
add_call 'transformation_matrix', @document.sizing.x_scale, 0, 0, @document.sizing.y_scale, 0, 0
|
9
9
|
add_call 'transformation_matrix', 1, 0, 0, 1, @document.sizing.x_offset, @document.sizing.y_offset
|
10
|
+
end
|
10
11
|
|
11
|
-
|
12
|
+
def container?
|
13
|
+
true
|
12
14
|
end
|
13
15
|
end
|
@@ -20,7 +20,10 @@ class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::Base
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def apply
|
23
|
-
raise SkipElementQuietly if
|
23
|
+
raise SkipElementQuietly if computed_properties.display == "none"
|
24
|
+
|
25
|
+
font = select_font
|
26
|
+
apply_font(font) if font
|
24
27
|
|
25
28
|
add_call_and_enter "text_group" if name == 'text'
|
26
29
|
|
@@ -28,22 +31,18 @@ class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::Base
|
|
28
31
|
add_call_and_enter "translate", document.distance(attributes['dx'] || 0), -document.distance(attributes['dy'] || 0)
|
29
32
|
end
|
30
33
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
34
|
+
# text_anchor isn't a Prawn option; we have to do some math to support it
|
35
|
+
# and so we handle this in Prawn::SVG::Interface#rewrite_call_arguments
|
36
|
+
opts = {
|
37
|
+
size: computed_properties.numerical_font_size,
|
38
|
+
style: font && font.subfamily,
|
39
|
+
text_anchor: computed_properties.text_anchor
|
40
|
+
}
|
36
41
|
|
37
|
-
|
38
|
-
|
39
|
-
if (anchor = attributes['text-anchor'] || state.text_anchor) &&
|
40
|
-
['start', 'middle', 'end'].include?(anchor)
|
41
|
-
opts[:text_anchor] = anchor
|
42
|
-
end
|
42
|
+
spacing = computed_properties.letter_spacing
|
43
|
+
spacing = spacing == 'normal' ? 0 : document.points(spacing)
|
43
44
|
|
44
|
-
|
45
|
-
add_call_and_enter 'character_spacing', document.points(spacing)
|
46
|
-
end
|
45
|
+
add_call_and_enter 'character_spacing', spacing
|
47
46
|
|
48
47
|
source.children.each do |child|
|
49
48
|
if child.node_type == :text
|
@@ -53,7 +52,6 @@ class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::Base
|
|
53
52
|
opts[:at] = [@x_positions.first, @y_positions.first]
|
54
53
|
|
55
54
|
if @x_positions.length > 1 || @y_positions.length > 1
|
56
|
-
# TODO : isn't this just text.shift ?
|
57
55
|
add_call 'draw_text', text[0..0], opts.dup
|
58
56
|
text = text[1..-1]
|
59
57
|
|
@@ -73,7 +71,6 @@ class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::Base
|
|
73
71
|
new_state.text_x_positions = @x_positions
|
74
72
|
new_state.text_y_positions = @y_positions
|
75
73
|
new_state.text_relative = @relative
|
76
|
-
new_state.text_anchor = opts[:text_anchor]
|
77
74
|
|
78
75
|
Prawn::SVG::Elements::Text.new(document, child, calls, new_state).process
|
79
76
|
|
@@ -83,5 +80,29 @@ class Prawn::SVG::Elements::Text < Prawn::SVG::Elements::Base
|
|
83
80
|
warnings << "Unknown tag '#{child.name}' inside text tag; ignoring"
|
84
81
|
end
|
85
82
|
end
|
83
|
+
|
84
|
+
# It's possible there was no text to render. In that case, add a 'noop' so
|
85
|
+
# character_spacing doesn't blow up when it finds it doesn't have a block to execute.
|
86
|
+
add_call 'noop' if calls.empty?
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def select_font
|
92
|
+
font_families = [computed_properties.font_family, document.fallback_font_name]
|
93
|
+
font_style = :italic if computed_properties.font_style == 'italic'
|
94
|
+
font_weight = Prawn::SVG::Font.weight_for_css_font_weight(computed_properties.font_weight)
|
95
|
+
|
96
|
+
font_families.compact.each do |name|
|
97
|
+
font = document.font_registry.load(name, font_weight, font_style)
|
98
|
+
return font if font
|
99
|
+
end
|
100
|
+
|
101
|
+
warnings << "Font family '#{computed_properties.font_family}' style '#{computed_properties.font_style}' is not a known font, and the fallback font could not be found."
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
def apply_font(font)
|
106
|
+
add_call 'font', font.name, style: font.subfamily
|
86
107
|
end
|
87
108
|
end
|
data/lib/prawn/svg/font.rb
CHANGED
@@ -11,12 +11,12 @@ class Prawn::SVG::Font
|
|
11
11
|
|
12
12
|
def self.weight_for_css_font_weight(weight)
|
13
13
|
case weight
|
14
|
-
when '100', '200', '300'
|
15
|
-
when '400', '500'
|
16
|
-
when '600'
|
17
|
-
when '700', 'bold'
|
18
|
-
when '800'
|
19
|
-
when '900'
|
14
|
+
when '100', '200', '300' then :light
|
15
|
+
when '400', '500', 'normal' then :normal
|
16
|
+
when '600' then :semibold
|
17
|
+
when '700', 'bold' then :bold
|
18
|
+
when '800' then :extrabold
|
19
|
+
when '900' then :black
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
data/lib/prawn/svg/interface.rb
CHANGED
@@ -169,6 +169,9 @@ module Prawn
|
|
169
169
|
# never want closepath to be automatically run as it stuffs up many drawing operations, such as dashes
|
170
170
|
# and line caps, and makes paths close that we didn't ask to be closed when fill is specified.
|
171
171
|
prawn.add_content 'B'
|
172
|
+
|
173
|
+
when 'noop'
|
174
|
+
yield
|
172
175
|
end
|
173
176
|
end
|
174
177
|
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Prawn::SVG::Pathable
|
2
|
+
Move = Struct.new(:destination)
|
3
|
+
Close = Struct.new(:destination)
|
4
|
+
Line = Struct.new(:destination)
|
5
|
+
Curve = Struct.new(:destination, :point1, :point2)
|
6
|
+
|
7
|
+
def bounding_box
|
8
|
+
points = commands.map { |command| translate(command.destination) }
|
9
|
+
x1, x2 = points.map(&:first).minmax
|
10
|
+
y2, y1 = points.map(&:last).minmax
|
11
|
+
|
12
|
+
[x1, y1, x2, y2]
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def apply_commands
|
18
|
+
commands.each do |command|
|
19
|
+
case command
|
20
|
+
when Move
|
21
|
+
add_call 'move_to', translate(command.destination)
|
22
|
+
when Close
|
23
|
+
add_call 'close_path'
|
24
|
+
when Line
|
25
|
+
add_call 'line_to', translate(command.destination)
|
26
|
+
when Curve
|
27
|
+
add_call 'curve_to', translate(command.destination), bounds: [translate(command.point1), translate(command.point2)]
|
28
|
+
else
|
29
|
+
raise NotImplementedError, "Unknown path command type"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def apply_markers
|
35
|
+
if marker = extract_element_from_url_id_reference(properties.marker_start, "marker")
|
36
|
+
marker.apply_marker(self, point: commands.first.destination, angle: angles.first)
|
37
|
+
end
|
38
|
+
|
39
|
+
if marker = extract_element_from_url_id_reference(properties.marker_mid, "marker")
|
40
|
+
(1..commands.length-2).each do |index|
|
41
|
+
marker.apply_marker(self, point: commands[index].destination, angle: angles[index])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
if marker = extract_element_from_url_id_reference(properties.marker_end, "marker")
|
46
|
+
marker.apply_marker(self, point: commands.last.destination, angle: angles.last)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def angles
|
51
|
+
return @angles if @angles
|
52
|
+
|
53
|
+
last_point = nil
|
54
|
+
|
55
|
+
destination_angles = commands.map do |command|
|
56
|
+
angles = case command
|
57
|
+
when Move
|
58
|
+
[nil, nil]
|
59
|
+
when Close, Line
|
60
|
+
angle = Math.atan2(command.destination[1] - last_point[1], command.destination[0] - last_point[0]) * 180.0 / Math::PI
|
61
|
+
[angle, angle]
|
62
|
+
when Curve
|
63
|
+
start = Math.atan2(command.point1[1] - last_point[1], command.point1[0] - last_point[0]) * 180.0 / Math::PI
|
64
|
+
stop = Math.atan2(command.destination[1] - command.point2[1], command.destination[0] - command.point2[0]) * 180.0 / Math::PI
|
65
|
+
[start, stop]
|
66
|
+
else
|
67
|
+
raise NotImplementedError, "Unknown path command type"
|
68
|
+
end
|
69
|
+
|
70
|
+
last_point = command.destination
|
71
|
+
angles
|
72
|
+
end
|
73
|
+
|
74
|
+
angles = destination_angles.each_cons(2).map do |first_angles, second_angles|
|
75
|
+
if first_angles.first.nil?
|
76
|
+
second_angles.first || 0
|
77
|
+
elsif second_angles.first.nil?
|
78
|
+
first_angles.last
|
79
|
+
else
|
80
|
+
first = first_angles.last
|
81
|
+
second = second_angles.last
|
82
|
+
bisect = (first + second) / 2.0
|
83
|
+
|
84
|
+
if (first - second).abs > 180
|
85
|
+
bisect >= 0 ? bisect - 180 : bisect + 180
|
86
|
+
else
|
87
|
+
bisect
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
if commands.last.is_a?(Close)
|
93
|
+
first = destination_angles.last.last || 0
|
94
|
+
second = angles.first
|
95
|
+
bisect = (first + second) / 2.0
|
96
|
+
|
97
|
+
angles << if (first - second).abs > 180
|
98
|
+
bisect >= 0 ? bisect - 180 : bisect + 180
|
99
|
+
else
|
100
|
+
bisect
|
101
|
+
end
|
102
|
+
else
|
103
|
+
angles << destination_angles.last.last
|
104
|
+
end
|
105
|
+
|
106
|
+
@angles = angles
|
107
|
+
end
|
108
|
+
|
109
|
+
def parse_points(points_string)
|
110
|
+
values = points_string.
|
111
|
+
to_s.
|
112
|
+
strip.
|
113
|
+
gsub(/(\d)-(\d)/, '\1 -\2').
|
114
|
+
split(Prawn::SVG::Elements::COMMA_WSP_REGEXP).
|
115
|
+
map(&:to_f)
|
116
|
+
|
117
|
+
if values.length % 2 == 1
|
118
|
+
document.warnings << "points attribute has an odd number of points; ignoring the last one"
|
119
|
+
values.pop
|
120
|
+
end
|
121
|
+
|
122
|
+
raise Prawn::SVG::Elements::Base::SkipElementQuietly if values.length == 0
|
123
|
+
|
124
|
+
values.each_slice(2).to_a
|
125
|
+
end
|
126
|
+
|
127
|
+
def translate(point)
|
128
|
+
[point[0].to_f, document.sizing.output_height - point[1].to_f]
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
class Prawn::SVG::Properties
|
2
|
+
Config = Struct.new(:default, :inheritable?, :keywords, :keyword_restricted?, :attr, :ivar)
|
3
|
+
|
4
|
+
EM = 16
|
5
|
+
FONT_SIZES = {
|
6
|
+
'xx-small' => EM / 4,
|
7
|
+
'x-small' => EM / 4 * 2,
|
8
|
+
'small' => EM / 4 * 3,
|
9
|
+
'medium' => EM / 4 * 4,
|
10
|
+
'large' => EM / 4 * 5,
|
11
|
+
'x-large' => EM / 4 * 6,
|
12
|
+
'xx-large' => EM / 4 * 7
|
13
|
+
}
|
14
|
+
|
15
|
+
PROPERTIES = {
|
16
|
+
"clip-path" => Config.new("none", false, %w(inherit none)),
|
17
|
+
"color" => Config.new('', true),
|
18
|
+
"display" => Config.new("inline", false, %w(inherit inline none), true),
|
19
|
+
"fill" => Config.new("black", true, %w(inherit none currentColor)),
|
20
|
+
"fill-opacity" => Config.new("1", true),
|
21
|
+
"font-family" => Config.new("sans-serif", true),
|
22
|
+
"font-size" => Config.new("medium", true, %w(inherit xx-small x-small small medium large x-large xx-large larger smaller)),
|
23
|
+
"font-style" => Config.new("normal", true, %w(inherit normal italic oblique), true),
|
24
|
+
"font-variant" => Config.new("normal", true, %w(inherit normal small-caps), true),
|
25
|
+
"font-weight" => Config.new("normal", true, %w(inherit normal bold 100 200 300 400 500 600 700 800 900), true), # bolder/lighter not supported
|
26
|
+
"letter-spacing" => Config.new("normal", true, %w(inherit normal)),
|
27
|
+
"marker-end" => Config.new("none", true, %w(inherit none)),
|
28
|
+
"marker-mid" => Config.new("none", true, %w(inherit none)),
|
29
|
+
"marker-start" => Config.new("none", true, %w(inherit none)),
|
30
|
+
"opacity" => Config.new("1", false),
|
31
|
+
"overflow" => Config.new('visible', false, %w(inherit visible hidden scroll auto), true),
|
32
|
+
"stop-color" => Config.new("black", false, %w(inherit none currentColor)),
|
33
|
+
"stroke" => Config.new("none", true, %w(inherit none currentColor)),
|
34
|
+
"stroke-dasharray" => Config.new("none", true, %w(inherit none)),
|
35
|
+
"stroke-linecap" => Config.new("butt", true, %w(inherit butt round square), true),
|
36
|
+
"stroke-opacity" => Config.new("1", true),
|
37
|
+
"stroke-width" => Config.new("1", true),
|
38
|
+
"text-anchor" => Config.new("start", true, %w(inherit start middle end), true),
|
39
|
+
}.freeze
|
40
|
+
|
41
|
+
PROPERTIES.each do |name, value|
|
42
|
+
value.attr = name.gsub("-", "_")
|
43
|
+
value.ivar = "@#{value.attr}"
|
44
|
+
end
|
45
|
+
|
46
|
+
PROPERTY_CONFIGS = PROPERTIES.values
|
47
|
+
NAMES = PROPERTIES.keys
|
48
|
+
ATTR_NAMES = PROPERTIES.keys.map { |name| name.gsub('-', '_') }
|
49
|
+
|
50
|
+
attr_accessor *ATTR_NAMES
|
51
|
+
|
52
|
+
def load_default_stylesheet
|
53
|
+
PROPERTY_CONFIGS.each do |config|
|
54
|
+
instance_variable_set(config.ivar, config.default)
|
55
|
+
end
|
56
|
+
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def set(name, value)
|
61
|
+
if config = PROPERTIES[name.to_s.downcase]
|
62
|
+
value = value.strip
|
63
|
+
keyword = value.downcase
|
64
|
+
keywords = config.keywords || ['inherit']
|
65
|
+
|
66
|
+
if keywords.include?(keyword)
|
67
|
+
value = keyword
|
68
|
+
elsif config.keyword_restricted?
|
69
|
+
value = config.default
|
70
|
+
end
|
71
|
+
|
72
|
+
instance_variable_set(config.ivar, value)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_h
|
77
|
+
PROPERTIES.each.with_object({}) do |(name, config), result|
|
78
|
+
result[name] = instance_variable_get(config.ivar)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def load_hash(hash)
|
83
|
+
hash.each { |name, value| set(name, value) if value }
|
84
|
+
end
|
85
|
+
|
86
|
+
def compute_properties(other)
|
87
|
+
PROPERTY_CONFIGS.each do |config|
|
88
|
+
value = other.send(config.attr)
|
89
|
+
|
90
|
+
if value && value != 'inherit'
|
91
|
+
value = compute_font_size_property(value).to_s if config.attr == "font_size"
|
92
|
+
instance_variable_set(config.ivar, value)
|
93
|
+
|
94
|
+
elsif value.nil? && !config.inheritable?
|
95
|
+
instance_variable_set(config.ivar, config.default)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def numerical_font_size
|
101
|
+
# px = pt for PDFs
|
102
|
+
FONT_SIZES[font_size] || font_size.to_f
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def compute_font_size_property(value)
|
108
|
+
if value[-1] == "%"
|
109
|
+
numerical_font_size * (value.to_f / 100.0)
|
110
|
+
elsif value == 'larger'
|
111
|
+
numerical_font_size + 4
|
112
|
+
elsif value == 'smaller'
|
113
|
+
numerical_font_size - 4
|
114
|
+
elsif value.match(/(\d|\.)em\z/i)
|
115
|
+
numerical_font_size * value.to_f
|
116
|
+
elsif value.match(/(\d|\.)rem\z/i)
|
117
|
+
value.to_f * EM
|
118
|
+
else
|
119
|
+
FONT_SIZES[value] || value.to_f
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
data/lib/prawn/svg/state.rb
CHANGED
@@ -1,39 +1,17 @@
|
|
1
1
|
class Prawn::SVG::State
|
2
2
|
attr_accessor :disable_drawing,
|
3
|
-
:
|
4
|
-
:
|
5
|
-
:
|
6
|
-
:fill_opacity, :stroke_opacity,
|
7
|
-
:fill, :stroke
|
3
|
+
:text_relative, :text_x_positions, :text_y_positions, :preserve_space,
|
4
|
+
:fill_opacity, :stroke_opacity, :stroke_width,
|
5
|
+
:computed_properties
|
8
6
|
|
9
7
|
def initialize
|
10
|
-
@
|
11
|
-
@stroke = false
|
8
|
+
@stroke_width = 1
|
12
9
|
@fill_opacity = 1
|
13
10
|
@stroke_opacity = 1
|
11
|
+
@computed_properties = Prawn::SVG::Properties.new.load_default_stylesheet
|
14
12
|
end
|
15
13
|
|
16
|
-
def
|
17
|
-
|
18
|
-
when 'fill' then @fill = true
|
19
|
-
when 'stroke' then @stroke = true
|
20
|
-
else raise
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def disable_draw_type(type)
|
25
|
-
case type
|
26
|
-
when 'fill' then @fill = false
|
27
|
-
when 'stroke' then @stroke = false
|
28
|
-
else raise
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def draw_type(type)
|
33
|
-
case type
|
34
|
-
when 'fill' then @fill
|
35
|
-
when 'stroke' then @stroke
|
36
|
-
else raise
|
37
|
-
end
|
14
|
+
def initialize_dup(other)
|
15
|
+
@computed_properties = @computed_properties.dup
|
38
16
|
end
|
39
17
|
end
|