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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +4 -0
  3. data/.rubocop.yml +18 -0
  4. data/.travis.yml +4 -1
  5. data/Gemfile +1 -1
  6. data/Guardfile +7 -2
  7. data/README.md +28 -17
  8. data/Rakefile +13 -6
  9. data/bin/bread-basket +14 -0
  10. data/bread-basket.gemspec +29 -24
  11. data/lib/bread/basket.rb +12 -6
  12. data/lib/bread/basket/cli.rb +10 -0
  13. data/lib/bread/basket/poster.rb +38 -0
  14. data/lib/bread/basket/poster/block_code_handler.rb +50 -0
  15. data/lib/bread/basket/poster/block_renderer.rb +14 -0
  16. data/lib/bread/basket/poster/box.rb +119 -0
  17. data/lib/bread/basket/poster/box_checker.rb +60 -0
  18. data/lib/bread/basket/poster/columns.rb +90 -0
  19. data/lib/bread/basket/poster/css_reader.rb +130 -0
  20. data/lib/bread/basket/poster/dimensions_helper.rb +123 -0
  21. data/lib/bread/basket/poster/header_callback.rb +29 -0
  22. data/lib/bread/basket/poster/header_maker.rb +74 -0
  23. data/lib/bread/basket/poster/image_box.rb +93 -0
  24. data/lib/bread/basket/poster/layout.rb +108 -0
  25. data/lib/bread/basket/poster/pdf_builder.rb +113 -0
  26. data/lib/bread/basket/poster/poster_maker.rb +55 -0
  27. data/lib/bread/basket/poster/prawn_patches/column_box.rb +17 -0
  28. data/lib/bread/basket/poster/text_renderer.rb +89 -0
  29. data/lib/bread/basket/poster/units_helper.rb +39 -0
  30. data/lib/bread/basket/version.rb +1 -1
  31. data/samples/block_sample.css +86 -0
  32. data/samples/flow_sample.css +68 -0
  33. data/samples/ipsum.jpg +0 -0
  34. data/samples/lorem.jpg +0 -0
  35. data/samples/lorem_block.md +86 -0
  36. data/samples/lorem_flow.md +58 -0
  37. data/samples/lorem_flow.pdf +3834 -6
  38. data/samples/sample.md +59 -0
  39. data/samples/simple.css +19 -0
  40. data/samples/simple.md +3 -0
  41. data/samples/ucair_logo.png +0 -0
  42. data/spec/cli_spec.rb +13 -0
  43. data/spec/poster/block_code_handler_spec.rb +47 -0
  44. data/spec/poster/box_spec.rb +114 -0
  45. data/spec/poster/columns_spec.rb +64 -0
  46. data/spec/poster/css_reader_spec.rb +115 -0
  47. data/spec/poster/header_maker_spec.rb +49 -0
  48. data/spec/poster/image_box_spec.rb +69 -0
  49. data/spec/poster/layout_spec.rb +75 -0
  50. data/spec/poster/pdf_builder_spec.rb +60 -0
  51. data/spec/poster/poster_maker_spec.rb +15 -0
  52. data/spec/poster/test_files/bad_file.md +1 -0
  53. data/spec/poster/test_files/basic_block.css +14 -0
  54. data/spec/poster/test_files/basic_flow.css +31 -0
  55. data/spec/poster/test_files/block_code_test.css +13 -0
  56. data/spec/poster/test_files/builder.css +39 -0
  57. data/spec/poster/test_files/circular.css +39 -0
  58. data/spec/poster/test_files/dragon.png +0 -0
  59. data/spec/poster/test_files/fitted_image.css +39 -0
  60. data/spec/poster/test_files/good_file.md +5 -0
  61. data/spec/poster/test_files/good_file.pdf +0 -0
  62. data/spec/poster/test_files/header_test.css +22 -0
  63. data/spec/poster/test_files/nearly_empty.css +10 -0
  64. data/spec/poster/test_files/self_referential.css +108 -0
  65. data/spec/poster/test_files/ucair_logo.png +0 -0
  66. data/spec/poster/text_renderer_spec.rb +54 -0
  67. data/spec/poster/units_helper_spec.rb +36 -0
  68. data/spec/spec_helper.rb +6 -0
  69. metadata +212 -34
  70. data/bread-basket-0.0.1.gem +0 -0
  71. 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