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