bread-basket 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +4 -0
- data/.rubocop.yml +18 -0
- data/.travis.yml +4 -1
- data/Gemfile +1 -1
- data/Guardfile +7 -2
- data/README.md +28 -17
- data/Rakefile +13 -6
- data/bin/bread-basket +14 -0
- data/bread-basket.gemspec +29 -24
- data/lib/bread/basket.rb +12 -6
- data/lib/bread/basket/cli.rb +10 -0
- data/lib/bread/basket/poster.rb +38 -0
- data/lib/bread/basket/poster/block_code_handler.rb +50 -0
- data/lib/bread/basket/poster/block_renderer.rb +14 -0
- data/lib/bread/basket/poster/box.rb +119 -0
- data/lib/bread/basket/poster/box_checker.rb +60 -0
- data/lib/bread/basket/poster/columns.rb +90 -0
- data/lib/bread/basket/poster/css_reader.rb +130 -0
- data/lib/bread/basket/poster/dimensions_helper.rb +123 -0
- data/lib/bread/basket/poster/header_callback.rb +29 -0
- data/lib/bread/basket/poster/header_maker.rb +74 -0
- data/lib/bread/basket/poster/image_box.rb +93 -0
- data/lib/bread/basket/poster/layout.rb +108 -0
- data/lib/bread/basket/poster/pdf_builder.rb +113 -0
- data/lib/bread/basket/poster/poster_maker.rb +55 -0
- data/lib/bread/basket/poster/prawn_patches/column_box.rb +17 -0
- data/lib/bread/basket/poster/text_renderer.rb +89 -0
- data/lib/bread/basket/poster/units_helper.rb +39 -0
- data/lib/bread/basket/version.rb +1 -1
- data/samples/block_sample.css +86 -0
- data/samples/flow_sample.css +68 -0
- data/samples/ipsum.jpg +0 -0
- data/samples/lorem.jpg +0 -0
- data/samples/lorem_block.md +86 -0
- data/samples/lorem_flow.md +58 -0
- data/samples/lorem_flow.pdf +3834 -6
- data/samples/sample.md +59 -0
- data/samples/simple.css +19 -0
- data/samples/simple.md +3 -0
- data/samples/ucair_logo.png +0 -0
- data/spec/cli_spec.rb +13 -0
- data/spec/poster/block_code_handler_spec.rb +47 -0
- data/spec/poster/box_spec.rb +114 -0
- data/spec/poster/columns_spec.rb +64 -0
- data/spec/poster/css_reader_spec.rb +115 -0
- data/spec/poster/header_maker_spec.rb +49 -0
- data/spec/poster/image_box_spec.rb +69 -0
- data/spec/poster/layout_spec.rb +75 -0
- data/spec/poster/pdf_builder_spec.rb +60 -0
- data/spec/poster/poster_maker_spec.rb +15 -0
- data/spec/poster/test_files/bad_file.md +1 -0
- data/spec/poster/test_files/basic_block.css +14 -0
- data/spec/poster/test_files/basic_flow.css +31 -0
- data/spec/poster/test_files/block_code_test.css +13 -0
- data/spec/poster/test_files/builder.css +39 -0
- data/spec/poster/test_files/circular.css +39 -0
- data/spec/poster/test_files/dragon.png +0 -0
- data/spec/poster/test_files/fitted_image.css +39 -0
- data/spec/poster/test_files/good_file.md +5 -0
- data/spec/poster/test_files/good_file.pdf +0 -0
- data/spec/poster/test_files/header_test.css +22 -0
- data/spec/poster/test_files/nearly_empty.css +10 -0
- data/spec/poster/test_files/self_referential.css +108 -0
- data/spec/poster/test_files/ucair_logo.png +0 -0
- data/spec/poster/text_renderer_spec.rb +54 -0
- data/spec/poster/units_helper_spec.rb +36 -0
- data/spec/spec_helper.rb +6 -0
- metadata +212 -34
- data/bread-basket-0.0.1.gem +0 -0
- data/spec/basket_spec.rb +0 -7
@@ -0,0 +1,60 @@
|
|
1
|
+
module Bread
|
2
|
+
module Basket
|
3
|
+
module Poster
|
4
|
+
class BoxChecker
|
5
|
+
# Determines which dimensions were provided and if provided dimensions
|
6
|
+
# are sufficient to determine left, right, top, and width of box
|
7
|
+
# bottom and height are optional but if top is missing are required to
|
8
|
+
# determine location of top.
|
9
|
+
attr_reader :box, :dimensions
|
10
|
+
|
11
|
+
def initialize(box, dimensions)
|
12
|
+
@box = box
|
13
|
+
@dimensions = dimensions
|
14
|
+
end
|
15
|
+
|
16
|
+
def horizontal_dimensions
|
17
|
+
arr = []
|
18
|
+
dimensions.each_key do |key|
|
19
|
+
arr << key if %w(left right width).include? key
|
20
|
+
end
|
21
|
+
warn_right if arr.length > 2
|
22
|
+
arr
|
23
|
+
end
|
24
|
+
|
25
|
+
def vertical_dimensions
|
26
|
+
arr = []
|
27
|
+
dimensions.each_key do |key|
|
28
|
+
arr << key if %w(top bottom height).include? key
|
29
|
+
end
|
30
|
+
warn_bottom if arr.length > 2
|
31
|
+
arr
|
32
|
+
end
|
33
|
+
|
34
|
+
def horizontal_ok?
|
35
|
+
horiz = %w(left right width) - horizontal_dimensions
|
36
|
+
horiz.length < 2
|
37
|
+
end
|
38
|
+
|
39
|
+
def vertical_ok?
|
40
|
+
bottom_and_height = %w(bottom height).all? { |s| dimensions.key? s }
|
41
|
+
dimensions.key?('top') || bottom_and_height
|
42
|
+
end
|
43
|
+
|
44
|
+
def box_ok?
|
45
|
+
horizontal_ok? && vertical_ok?
|
46
|
+
end
|
47
|
+
|
48
|
+
def warn_right
|
49
|
+
puts "Warning: For selector #{box.selector_name}, left, width AND right were " \
|
50
|
+
'provided. Right will be ignored.'
|
51
|
+
end
|
52
|
+
|
53
|
+
def warn_bottom
|
54
|
+
puts "Warning: For selector #{box.selector_name}, top, height AND bottom were " \
|
55
|
+
'provided. Bottom will be ignored.'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Bread
|
2
|
+
module Basket
|
3
|
+
module Poster
|
4
|
+
class Columns
|
5
|
+
# Columns are a special type of bounding box in prawn so they get their
|
6
|
+
# own class here to handle their dimensions.
|
7
|
+
attr_reader :specs, :count, :layout, :tops, :width, :lefts, :boxes, :spacing
|
8
|
+
|
9
|
+
def initialize(specs, layout)
|
10
|
+
@specs = specs
|
11
|
+
@layout = layout
|
12
|
+
check_specs
|
13
|
+
@count = specs['count'].to_i
|
14
|
+
@spacing = specs['font-size'] || layout.font_size
|
15
|
+
init_tops
|
16
|
+
init_widths
|
17
|
+
init_lefts
|
18
|
+
create_boxes
|
19
|
+
end
|
20
|
+
|
21
|
+
def init_tops
|
22
|
+
if specs['top'].is_a? Array
|
23
|
+
@tops = tops_from_arr
|
24
|
+
else
|
25
|
+
@tops = Array.new(count, specs['top'])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def tops_from_arr
|
30
|
+
case specs['top'].length
|
31
|
+
when (count / 2.0).ceil
|
32
|
+
symmetric_tops!
|
33
|
+
when count
|
34
|
+
specs['top']
|
35
|
+
else
|
36
|
+
message = 'Columns top specification has wrong length'
|
37
|
+
layout.give_up(message)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def symmetric_tops!
|
42
|
+
arr = specs['top'].clone
|
43
|
+
specs['top'].pop if count.odd?
|
44
|
+
specs['top'].reverse!
|
45
|
+
arr + specs['top']
|
46
|
+
end
|
47
|
+
|
48
|
+
def init_widths
|
49
|
+
col_spacing = spacing * (count - 1) # from Prawn
|
50
|
+
@width = (columns_width - col_spacing) / count.to_f
|
51
|
+
end
|
52
|
+
|
53
|
+
def columns_width
|
54
|
+
total_margin_space = 2 * layout.margin
|
55
|
+
layout.width - total_margin_space
|
56
|
+
end
|
57
|
+
|
58
|
+
def init_lefts
|
59
|
+
column_width = width + spacing # again from Prawn
|
60
|
+
@lefts = count.times.inject([layout.margin]) do |a|
|
61
|
+
a << a[-1] + column_width
|
62
|
+
end
|
63
|
+
@lefts.pop
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_boxes
|
67
|
+
@boxes = []
|
68
|
+
count.times do |index|
|
69
|
+
box = Box.new "columns[#{index}]",
|
70
|
+
layout,
|
71
|
+
'top' => tops[index],
|
72
|
+
'left' => lefts[index],
|
73
|
+
'width' => width,
|
74
|
+
'bottom' => layout.margin
|
75
|
+
@boxes << box
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def check_specs
|
80
|
+
message = '.columns selector must include top and count to be valid'
|
81
|
+
layout.give_up(message) unless top_and_count?
|
82
|
+
end
|
83
|
+
|
84
|
+
def top_and_count?
|
85
|
+
%w(top count).all? { |attr| specs.key? attr }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Bread
|
2
|
+
module Basket
|
3
|
+
module Poster
|
4
|
+
class CSSReader
|
5
|
+
# The CSSReader reads in the css and then creates:
|
6
|
+
# - layout
|
7
|
+
# - columns (if flow)
|
8
|
+
# - boxes (Box or ImageBox)
|
9
|
+
# and then attaches #-styles to the layout help
|
10
|
+
include CssParser
|
11
|
+
include UnitsHelper
|
12
|
+
attr_reader :parser, :layout
|
13
|
+
|
14
|
+
COLUMNS_FAIL = 'To use a flow layout your stylesheet must include ' \
|
15
|
+
'.columns selector.'
|
16
|
+
LAYOUT_FAIL = 'Your stylesheet must include the .layout selector.'
|
17
|
+
|
18
|
+
def initialize(stylesheet_path, layout)
|
19
|
+
@layout = layout
|
20
|
+
@parser = CssParser::Parser.new
|
21
|
+
@parser.load_file!(stylesheet_path)
|
22
|
+
end
|
23
|
+
|
24
|
+
def do_your_thing!
|
25
|
+
init_layout
|
26
|
+
create_columns if layout.flow?
|
27
|
+
create_bounding_boxes
|
28
|
+
create_styles
|
29
|
+
end
|
30
|
+
|
31
|
+
def init_layout
|
32
|
+
layout_rules = parser.find_by_selector '.layout'
|
33
|
+
layout.give_up LAYOUT_FAIL if layout_rules.empty?
|
34
|
+
specs = rules_to_specs(layout_rules[0])
|
35
|
+
create_layout_attributes(specs)
|
36
|
+
layout.handle_defaults
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_layout_attributes(specs)
|
40
|
+
attributes_from_specs(specs)
|
41
|
+
finish_layout_box
|
42
|
+
end
|
43
|
+
|
44
|
+
def attributes_from_specs(specs)
|
45
|
+
layout.width = specs.delete 'width'
|
46
|
+
layout.height = specs.delete 'height'
|
47
|
+
layout.margin = specs.delete 'margin'
|
48
|
+
specs.each do |k, v|
|
49
|
+
layout.create_attribute(k, v)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def finish_layout_box
|
54
|
+
layout.left = 0
|
55
|
+
layout.right = layout.width
|
56
|
+
layout.bottom = 0
|
57
|
+
layout.top = layout.height
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_columns
|
61
|
+
columns = parser.find_by_selector '.columns'
|
62
|
+
layout.give_up COLUMNS_FAIL if columns.empty?
|
63
|
+
specs = rules_to_specs(columns[0])
|
64
|
+
columns = Columns.new specs, layout
|
65
|
+
layout.create_attribute('columns', columns.boxes)
|
66
|
+
layout.create_attribute('column_styles', specs)
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_bounding_boxes
|
70
|
+
parser.each_selector do |selector, declarations|
|
71
|
+
next if skip_selector? selector
|
72
|
+
method_name = to_method_name selector
|
73
|
+
specs = rules_to_specs declarations
|
74
|
+
box = create_box selector, layout, specs
|
75
|
+
layout.create_attribute(method_name, box)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def skip_selector?(selector)
|
80
|
+
old_selector = %w(.layout .columns).include? selector
|
81
|
+
hash_selector = selector =~ HASH_SELECTOR_REGEX
|
82
|
+
old_selector || hash_selector
|
83
|
+
end
|
84
|
+
|
85
|
+
def create_box(selector, layout, specs)
|
86
|
+
if specs.key?('src')
|
87
|
+
ImageBox.new selector, layout, specs
|
88
|
+
else
|
89
|
+
Box.new selector, layout, specs
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def create_styles
|
94
|
+
parser.each_selector do |selector, declarations|
|
95
|
+
next unless selector =~ HASH_SELECTOR_REGEX
|
96
|
+
method_name = to_method_name selector
|
97
|
+
specs = rules_to_specs declarations
|
98
|
+
layout.create_attribute(method_name, specs)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def rules_to_specs(css_rules)
|
103
|
+
# rule => "top: title.bottom, authors.bottom"
|
104
|
+
# spec => { top: ['title.bottom', 'authors.bottom'] }
|
105
|
+
rules_arr = css_rules.split(';')
|
106
|
+
hash = {}
|
107
|
+
|
108
|
+
rules_arr.each { |rule| rule_to_hash(rule, hash) }
|
109
|
+
|
110
|
+
hash.each_with_object({}) do |(k, v), h|
|
111
|
+
arr = v.map { |value| convert_units(value) }
|
112
|
+
h[k] = arr.length == 1 ? arr[0] : arr
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def rule_to_hash(rule, hash)
|
117
|
+
rule_arr = rule.split(':')
|
118
|
+
key = rule_arr[0].strip
|
119
|
+
value_arr = rule_arr[1].strip.split(',')
|
120
|
+
value_arr.map!(&:strip)
|
121
|
+
hash[key] = value_arr
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_method_name(hash_selector)
|
125
|
+
hash_selector.sub('#', '').sub('.', '').gsub('-', '_')
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module Bread
|
2
|
+
module Basket
|
3
|
+
module Poster
|
4
|
+
class DimensionsHelper
|
5
|
+
# Dimensions Helper checks the dimensions and determines how to handle
|
6
|
+
# them (pending vs numeric)
|
7
|
+
include UnitsHelper
|
8
|
+
attr_reader :box, :layout, :dimensions, :checker
|
9
|
+
|
10
|
+
def initialize(box, layout, dimensions)
|
11
|
+
@layout = layout
|
12
|
+
@box = box
|
13
|
+
@dimensions = dimensions
|
14
|
+
@checker = BoxChecker.new box, dimensions
|
15
|
+
fail_dimensions unless checker.box_ok?
|
16
|
+
determine_dimensions
|
17
|
+
box.try_to_resolve
|
18
|
+
frame_box
|
19
|
+
end
|
20
|
+
|
21
|
+
def fail_dimensions
|
22
|
+
message = "For the selector #{box.selector_name}, you failed to " \
|
23
|
+
'provide enough dimensions to draw a box. You need two of these three: ' \
|
24
|
+
'left, right, width; and either `top` or `bottom and height`. Instead you ' \
|
25
|
+
"provided #{dimensions}."
|
26
|
+
layout.give_up message
|
27
|
+
end
|
28
|
+
|
29
|
+
def determine_dimensions
|
30
|
+
dimensions.each do |key, value|
|
31
|
+
next unless DIMENSIONS.include? key
|
32
|
+
case value
|
33
|
+
when String
|
34
|
+
create_pending_dimension key, value
|
35
|
+
when Numeric
|
36
|
+
add_numeric_dimension key, value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_numeric_dimension(dimension_name, value)
|
42
|
+
box.instance_variable_set "@#{dimension_name}", value
|
43
|
+
box.add_to_determined(dimension_name, value)
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_pending_dimension(dimension_name, command_string)
|
47
|
+
box.pending << dimension_name
|
48
|
+
commands = command_array command_string
|
49
|
+
pending = create_pending_hash commands
|
50
|
+
return_hash = { pending: pending, command: commands }
|
51
|
+
box.instance_variable_set "@#{dimension_name}", return_hash
|
52
|
+
end
|
53
|
+
|
54
|
+
def command_array(command_string)
|
55
|
+
commands = command_string.split(' ')
|
56
|
+
commands.map { |command| convert_command(command) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def convert_command(command)
|
60
|
+
case command
|
61
|
+
when '*', '+', '-', '/'
|
62
|
+
command.to_sym
|
63
|
+
when layout.css_reader.class::NUMERIC_REGEX
|
64
|
+
Float command
|
65
|
+
else
|
66
|
+
command.gsub('-', '_')
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_pending_hash(command_array)
|
71
|
+
h = {}
|
72
|
+
command_array.each_with_index do |command, index|
|
73
|
+
if (command.is_a? String) && (command != '(') && (command != ')')
|
74
|
+
h[command] = index
|
75
|
+
end
|
76
|
+
end
|
77
|
+
h
|
78
|
+
end
|
79
|
+
|
80
|
+
def frame_box
|
81
|
+
# from two of left, right, width, determine other dimension
|
82
|
+
# bottom, height are optional unless top is missing
|
83
|
+
left_right_width
|
84
|
+
top_bottom_height
|
85
|
+
end
|
86
|
+
|
87
|
+
def left_right_width
|
88
|
+
given = checker.horizontal_dimensions
|
89
|
+
difference = %w(left right width) - given
|
90
|
+
missing = difference.empty? ? 'right' : difference[0]
|
91
|
+
missing_dimension given, missing
|
92
|
+
end
|
93
|
+
|
94
|
+
def top_bottom_height
|
95
|
+
given = checker.vertical_dimensions
|
96
|
+
difference = %w(top bottom height) - given
|
97
|
+
case difference.length
|
98
|
+
when 0
|
99
|
+
missing_dimension given, 'bottom'
|
100
|
+
when 1
|
101
|
+
missing_dimension given, difference[0]
|
102
|
+
when 2
|
103
|
+
make_stretchy
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def missing_dimension(given, missing)
|
108
|
+
if given.any? { |dim| box.pending.include? dim }
|
109
|
+
box.unfinished << missing
|
110
|
+
else
|
111
|
+
box.determine_missing missing
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def make_stretchy
|
116
|
+
box.add_to_determined 'bottom', :stretchy
|
117
|
+
box.add_to_determined 'height', :stretchy
|
118
|
+
box.stretchy = true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Bread
|
2
|
+
module Basket
|
3
|
+
module Poster
|
4
|
+
class HeaderCallback
|
5
|
+
def initialize(document, options)
|
6
|
+
@color = options['background-color']
|
7
|
+
@document = document
|
8
|
+
@radius = options['radius']
|
9
|
+
@height = options['height']
|
10
|
+
@width = options['width']
|
11
|
+
end
|
12
|
+
|
13
|
+
def render_behind(fragment)
|
14
|
+
original_color = @document.fill_color
|
15
|
+
vert_dist = (@height - fragment.height) / 2
|
16
|
+
|
17
|
+
# TODO: If you want to get real picky, the vertical centering should
|
18
|
+
# be aware of whether the text has ascenders or descenders, there's
|
19
|
+
# a prawn method for calculating that sort of thing precisely.
|
20
|
+
left_top = [@document.bounds.left, fragment.top + vert_dist]
|
21
|
+
|
22
|
+
@document.fill_color = @color.sub('#', '')
|
23
|
+
@document.fill_rounded_rectangle(left_top, @width, @height, @radius)
|
24
|
+
@document.fill_color = original_color
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Bread
|
2
|
+
module Basket
|
3
|
+
module Poster
|
4
|
+
class HeaderMaker
|
5
|
+
attr_reader :pdf, :layout, :styles
|
6
|
+
|
7
|
+
HEADER_DEFAULTS =
|
8
|
+
{ 'font-size' => 36,
|
9
|
+
'color' => '000000',
|
10
|
+
'margin-top' => 0,
|
11
|
+
'margin-bottom' => 0,
|
12
|
+
'radius' => 0,
|
13
|
+
'background-color' => 'ffffff',
|
14
|
+
'font-family' => 'Helvetica'
|
15
|
+
}
|
16
|
+
|
17
|
+
def initialize(pdf, layout)
|
18
|
+
@pdf = pdf
|
19
|
+
@layout = layout
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_header(text, styles)
|
23
|
+
@text = text
|
24
|
+
@styles = init_styles(styles)
|
25
|
+
@header_callback = Poster::HeaderCallback.new(pdf, @styles)
|
26
|
+
|
27
|
+
pdf.move_down top_margin
|
28
|
+
pdf.formatted_text_box text_box_arr, text_box_opts
|
29
|
+
pdf.move_down bottom_margin
|
30
|
+
|
31
|
+
text # don't return a Numeric, Redcarpet hates that!
|
32
|
+
end
|
33
|
+
|
34
|
+
def init_styles(styles)
|
35
|
+
styles = HEADER_DEFAULTS.merge layout_styles
|
36
|
+
styles['width'] = layout.columns[0].width unless styles.key? 'width'
|
37
|
+
styles['height'] = styles['font-size'] * 2 unless styles.key? 'height'
|
38
|
+
styles
|
39
|
+
end
|
40
|
+
|
41
|
+
def layout_styles
|
42
|
+
if layout.respond_to? :header
|
43
|
+
layout.header
|
44
|
+
else
|
45
|
+
{}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def text_box_arr
|
50
|
+
[{ text: @text,
|
51
|
+
callback: @header_callback,
|
52
|
+
color: styles['color'].sub('#', ''),
|
53
|
+
font: styles['font-family'],
|
54
|
+
size: styles['font-size']
|
55
|
+
}]
|
56
|
+
end
|
57
|
+
|
58
|
+
def text_box_opts
|
59
|
+
{ at: [pdf.bounds.left, pdf.cursor],
|
60
|
+
width: layout.columns[0].width,
|
61
|
+
align: :center }
|
62
|
+
end
|
63
|
+
|
64
|
+
def top_margin
|
65
|
+
styles['margin-top'] + (styles['height'] - styles['font-size']) / 2
|
66
|
+
end
|
67
|
+
|
68
|
+
def bottom_margin
|
69
|
+
styles['margin-bottom'] + styles['height']
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|