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