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,93 @@
1
+ module Bread
2
+ module Basket
3
+ module Poster
4
+ class ImageBox < Box
5
+ attr_reader :size
6
+ attr_accessor :box_width, :box_height, :im_width, :im_height
7
+ def setup_dimensions
8
+ image_specs
9
+ handle_width_and_height
10
+ DimensionsHelper.new(self, layout, specs)
11
+ layout.pending << selector_name unless pending.empty?
12
+ end
13
+
14
+ def image_specs
15
+ image_path = Bread::Basket::Poster.dir_path + '/' + specs['src']
16
+ @size = FastImage.size image_path
17
+ read_fail(image_path) if size.nil?
18
+ # TODO: Learn how printing resolution in images works with prawn
19
+ # update: it's complicated!
20
+ end
21
+
22
+ def handle_width_and_height
23
+ if specs['width'] || specs['height']
24
+ # shuffle values around for fitting
25
+ fitted_width_and_height
26
+ else
27
+ specs['width'] = size[0]
28
+ specs['height'] = size[1]
29
+ end
30
+ end
31
+
32
+ def fitted_width_and_height
33
+ # set width and height from size if missing from css
34
+ find_width_height
35
+ # store these values as specifying the container's size, not
36
+ # the image's actual final size
37
+ specs['box_width'] = specs['width']
38
+ specs['box_height'] = specs['height']
39
+ # now use css notation to make this dependent on its own fitting so
40
+ # that other Dimensions Mechanics can handle this normally
41
+ specs['width'] = "#{method_name}.im_width"
42
+ specs['height'] = "#{method_name}.im_height"
43
+ end
44
+
45
+ def find_width_height
46
+ specs['width'] ||= size[0]
47
+ specs['height'] ||= size[1]
48
+ end
49
+
50
+ def try_to_resolve
51
+ if box_width.is_a?(Numeric) && box_height.is_a?(Numeric)
52
+ fit_width_and_height
53
+ end
54
+ super
55
+ end
56
+
57
+ def fit_width_and_height
58
+ if im_ratio > box_ratio
59
+ w = box_width
60
+ h = box_width / im_ratio
61
+ else
62
+ h = box_height
63
+ w = box_height * im_ratio
64
+ end
65
+ add_to_determined('im_width', w)
66
+ add_to_determined('im_height', h)
67
+ end
68
+
69
+ def box_ratio
70
+ box_width / box_height.to_f
71
+ end
72
+
73
+ def im_ratio
74
+ size[0] / size[1].to_f
75
+ end
76
+
77
+ def read_fail(image_path)
78
+ message = "Couldn't find image for #{selector_name} at #{image_path}."
79
+ layout.give_up(message)
80
+ end
81
+
82
+ def inspect
83
+ str = ''
84
+ %w(top left width height bottom right box_width box_height
85
+ im_width im_height).each do |dim|
86
+ str << "#{dim}: #{send(dim)}; "
87
+ end
88
+ str.strip
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,108 @@
1
+ module Bread
2
+ module Basket
3
+ module Poster
4
+ class Layout
5
+ attr_reader :metadata, :stylesheet, :css_reader, :type
6
+ attr_accessor :height, :width, :left, :right, :top, :bottom, :margin,
7
+ :pending, :determined, :font_size, :font_family, :boxes,
8
+ :image_boxes
9
+
10
+ def initialize(metadata)
11
+ @metadata = metadata
12
+ @type = determine_type
13
+ @stylesheet = find_stylesheet(metadata['stylesheet'])
14
+ @css_reader = CSSReader.new(stylesheet, self)
15
+ css_reader.do_your_thing!
16
+ end
17
+
18
+ def determine_type
19
+ case @metadata['layout']
20
+ when 'block'
21
+ :block
22
+ when 'flow'
23
+ :flow
24
+ else
25
+ handle_else @metadata['layout']
26
+ :flow
27
+ end
28
+ end
29
+
30
+ def find_stylesheet(stylesheet_name)
31
+ if stylesheet_name
32
+ path = Bread::Basket::Poster.dir_path + '/' + stylesheet_name
33
+ path += '.css' unless stylesheet_name.include?('.css')
34
+ path
35
+ else
36
+ puts 'Warning: no stylesheet given, using template instead.'
37
+ template
38
+ end
39
+ end
40
+
41
+ def flow?
42
+ type == :flow
43
+ end
44
+
45
+ def template
46
+ template = File.expand_path('./samples/block_sample.css')
47
+ template = File.expand_path('./samples/flow_sample.css') if flow?
48
+ template
49
+ end
50
+
51
+ def handle_else(layout)
52
+ if layout
53
+ puts "Warning: Unrecognized layout `#{layout}`, using flow instead"
54
+ else
55
+ puts 'Warning: No layout specified, defaulting to flow.'
56
+ end
57
+ end
58
+
59
+ def create_attribute(key, value)
60
+ new_key = key.gsub('-', '_').sub('.', '').to_sym
61
+ define_singleton_method(new_key) { value }
62
+ # This is called at end of each attribute's definition
63
+ try_to_resolve_pendings unless pending.nil?
64
+ end
65
+
66
+ def handle_defaults
67
+ empty_defaults
68
+ @font_size ||= 36
69
+ @font_family ||= 'Helvetica'
70
+ @margin ||= 36
71
+ # add dimensions to the determined hash for reference
72
+ # open to a better solution here :)
73
+ %w(width height left right top bottom margin font_size
74
+ font_family).each do |method_name|
75
+ determined[method_name] = eval("@#{method_name}")
76
+ end
77
+ end
78
+
79
+ def empty_defaults
80
+ @pending = []
81
+ @determined = {}
82
+ @boxes = []
83
+ @image_boxes = []
84
+ end
85
+
86
+ def try_to_resolve_pendings
87
+ pending.reverse_each do |name|
88
+ # very limited match on columns for now
89
+ match = name.match(/columns\[(\d)\]/)
90
+ if match
91
+ index = match[1].to_i
92
+ columns[index].try_to_resolve
93
+ else
94
+ box = send name.sub('.', '').gsub('-', '_')
95
+ box.try_to_resolve
96
+ end
97
+ end
98
+ end
99
+
100
+ def give_up(message)
101
+ puts '== Aborting =='
102
+ puts message
103
+ exit
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,113 @@
1
+ module Bread
2
+ module Basket
3
+ module Poster
4
+ class PDFBuilder
5
+ attr_reader :layout, :pdf, :text_renderer, :block_renderer, :wait_list
6
+
7
+ def initialize
8
+ @layout = Poster.layout
9
+ @wait_list = []
10
+ create_pdf
11
+ create_renderers
12
+ end
13
+
14
+ def create_pdf
15
+ page_size = [layout.width, layout.height]
16
+ @pdf = Prawn::Document.new(page_size: page_size, margin: 0)
17
+ Poster.pdf = @pdf
18
+ end
19
+
20
+ def create_renderers
21
+ @text_renderer = Redcarpet::Markdown.new(TextRenderer, TEXT_RENDERER_OPTS)
22
+ # @block_renderer = Redcarpet::Markdown.new(BlockRenderer)
23
+ end
24
+
25
+ def build
26
+ layout.type == :block ? build_blocks : build_flow
27
+ end
28
+
29
+ def build_blocks
30
+ # block_renderer.render layout.body
31
+ end
32
+
33
+ def build_flow
34
+ image_boxes
35
+ boxes_from_metadata
36
+ create_columns
37
+ end
38
+
39
+ def boxes_from_metadata
40
+ layout.metadata.each do |key, value|
41
+ next unless layout.boxes.include? '.' + key
42
+ box = layout.send key.gsub('-', '_')
43
+ box.content = value.to_s
44
+ try_to_build box
45
+ try_wait_list # rename to try wait list and rewrite a little
46
+ end
47
+ end
48
+
49
+ def try_to_build(box)
50
+ if box.pending.empty?
51
+ pdf_box = build_box(box)
52
+ finish_stretchy_box(box, pdf_box) if box.stretchy?
53
+ else
54
+ wait_list << box unless wait_list.include? box
55
+ end
56
+ end
57
+
58
+ def finish_stretchy_box(box, pdf_box)
59
+ box.height = pdf_box.height
60
+ box.determine_missing('bottom')
61
+ end
62
+
63
+ def try_wait_list
64
+ layout.try_to_resolve_pendings
65
+ wait_list.each { |box| try_to_build box }
66
+ end
67
+
68
+ def build_box(box)
69
+ opts = { width: box.width }
70
+ opts[:height] = box.height if box.height.is_a? Numeric
71
+
72
+ pdf.bounding_box([box.left, box.top], opts) do
73
+ Poster.current_styles = box.styles
74
+ text_renderer.render box.content
75
+ end
76
+ end
77
+
78
+ def create_columns
79
+ pdf.column_box(left_top, column_box_opts) do
80
+ Poster.current_styles = layout.column_styles
81
+ text_renderer.render Poster.body
82
+ end
83
+ end
84
+
85
+ def left_top
86
+ left_edge = layout.columns[0].left
87
+ top_edge = layout.columns[0].top
88
+ [left_edge, top_edge]
89
+ end
90
+
91
+ def column_box_opts
92
+ columns_width = pdf.bounds.width - 2 * layout.margin
93
+ columns_height = left_top[1] - layout.margin
94
+ { columns: 4, width: columns_width, height: columns_height }
95
+ end
96
+
97
+ def image_boxes
98
+ layout.image_boxes.each do |box_name|
99
+ image_box = layout.send box_name
100
+ path = image_path(image_box)
101
+ pdf.image path, fit: [image_box.width, image_box.height],
102
+ at: [image_box.left, image_box.top]
103
+ end
104
+ end
105
+
106
+ def image_path(image_box)
107
+ image_src = image_box.styles['src']
108
+ Bread::Basket::Poster.dir_path + '/' + image_src
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,55 @@
1
+ module Bread
2
+ module Basket
3
+ module Poster
4
+ class PosterMaker
5
+ YAML_REGEX = /^(---\s*\n.*?\n?)^(---\s*$\n?)/m
6
+ ERROR_MESSAGE = <<-EOS
7
+
8
+
9
+ Your file must start with a YAML front matter.
10
+ For example:
11
+ ---
12
+ layout: flow
13
+ stylesheet: my_template
14
+ ---
15
+
16
+ EOS
17
+
18
+ attr_accessor :layout, :metadata, :body
19
+
20
+ def initialize(filename)
21
+ @filename = filename
22
+ filepath = File.expand_path(filename)
23
+ Poster.dir_path = File.dirname(filepath)
24
+ check_file
25
+ create_layout
26
+ PDFBuilder.new.build
27
+ render
28
+ end
29
+
30
+ def render
31
+ name = @filename.sub(/\..*/, '')
32
+ Poster.pdf.render_file name + '.pdf'
33
+ end
34
+
35
+ def check_file
36
+ source = File.read(@filename)
37
+ @matchdata = source.match(YAML_REGEX)
38
+ fail NoFrontMatterError, ERROR_MESSAGE unless @matchdata
39
+ end
40
+
41
+ def create_layout
42
+ @metadata = YAML.load(@matchdata[0])
43
+ @body = @matchdata.post_match
44
+ Poster.body = @body
45
+ @layout = Layout.new(@metadata)
46
+ Poster.layout = layout
47
+ end
48
+ end
49
+
50
+ # define custom error so that specs only pass if
51
+ # error is caused by lack of front matter
52
+ class NoFrontMatterError < StandardError; end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,17 @@
1
+ module Prawn
2
+ class Document
3
+ class ColumnBox < BoundingBox
4
+ # :nocov:
5
+ # Not testing this because I can't figure out how to do it :(
6
+ # so be sure to look at samples to make sure column tops are correct
7
+ def move_past_bottom
8
+ @current_column = (@current_column + 1) % @columns
9
+ column = ::Bread::Basket::Poster.layout.columns[@current_column]
10
+ @document.y = column.top
11
+ @document.start_new_page if @current_column == 0
12
+ @y = @parent.absolute_top if @reflow_margins && (@current_column == 0)
13
+ end
14
+ # :nocov:
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,89 @@
1
+ module Bread
2
+ module Basket
3
+ module Poster
4
+ class TextRenderer < Redcarpet::Render::Base
5
+ attr_reader :pdf, :layout, :curr_styles
6
+
7
+ def preprocess(full_document)
8
+ @pdf = Poster.pdf
9
+ @layout = Poster.layout
10
+ @curr_styles = Poster.current_styles
11
+ curr_styles['font-size'] = layout.font_size unless curr_styles.key? 'font-size'
12
+ pdf.font_size curr_styles['font-size']
13
+ full_document
14
+ end
15
+
16
+ def alignment
17
+ if curr_styles['text-align']
18
+ curr_styles['text-align'].to_sym
19
+ else
20
+ :left
21
+ end
22
+ end
23
+
24
+ def paragraph(text)
25
+ pdf.text text, inline_format: true, align: alignment
26
+ end
27
+
28
+ def header(text, _header_level)
29
+ @header_maker = Poster::HeaderMaker.new(pdf, layout) unless @header_maker
30
+ @header_maker.create_header(text, curr_styles)
31
+ end
32
+
33
+ # Overloaded function for images, equations and maybe someday even code
34
+ def block_code(content, first_line)
35
+ BlockCodeHandler.new(pdf, layout, first_line, content)
36
+ ''
37
+ end
38
+
39
+ # > Quote Feature
40
+ # def block_quote(_quote)
41
+ # end
42
+
43
+ # *italic*
44
+ def emphasis(text)
45
+ "<i>#{text}</i>"
46
+ end
47
+
48
+ # **bold**
49
+ def double_emphasis(text)
50
+ "<b>#{text}</b>"
51
+ end
52
+
53
+ # _underline_
54
+ def underline(text)
55
+ "<u>#{text}</u>"
56
+ end
57
+
58
+ # super^script
59
+ def superscript(text)
60
+ "<sup>#{text}</sup>"
61
+ end
62
+
63
+ # inline `code span`
64
+ # def codespan(_text)
65
+ # end
66
+
67
+ # inline ==highlighting==
68
+ def highlight(text)
69
+ color = layout.respond_to?(:highlight) ? layout.highlight['color'] : '#f700ff'
70
+ color.sub!('#', '')
71
+ "<color rgb='#{color}'>#{text}</color>"
72
+ end
73
+
74
+ # first thing called on each element, it's return
75
+ # value gets added to the table row
76
+ # def table_cell(_content, _alignment)
77
+ # end
78
+
79
+ # called after all cells, content is their contents joined
80
+ # def table_row(_content)
81
+ # end
82
+
83
+ # called after all rows, header is header row, body is remaining rows.
84
+ # def table(_header, _body)
85
+ # end
86
+ end
87
+ end
88
+ end
89
+ end