bread-basket 0.0.0 → 0.0.1

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 (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