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