prawn-markup 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ module Prawn
2
+ module Markup
3
+ module Elements
4
+ class Cell < Item
5
+ attr_reader :header, :width
6
+
7
+ def initialize(header: false, width: 'auto')
8
+ super()
9
+ @header = header
10
+ @width = width
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Prawn
2
+ module Markup
3
+ module Elements
4
+ class Item
5
+ attr_reader :nodes
6
+
7
+ def initialize
8
+ @nodes = []
9
+ end
10
+
11
+ def single?
12
+ nodes.size <= 1
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ module Prawn
2
+ module Markup
3
+ module Elements
4
+ class List
5
+ attr_reader :ordered, :items
6
+
7
+ def initialize(ordered)
8
+ @ordered = ordered
9
+ @items = []
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ module Prawn
2
+ module Markup
3
+ module Interface
4
+ attr_writer :markup_options
5
+
6
+ def markup(html, options = {})
7
+ options = HashMerger.deep(markup_options, options)
8
+ Processor.new(self, options).parse(html)
9
+ end
10
+
11
+ def markup_options
12
+ @markup_options ||= {}
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,145 @@
1
+ module Prawn
2
+ module Markup
3
+ # Processes known HTML tags. Unknown tags are ignored.
4
+ class Processor < Nokogiri::XML::SAX::Document
5
+ class Error < StandardError; end
6
+
7
+ class << self
8
+ def known_elements
9
+ @@known_elments ||= []
10
+ end
11
+
12
+ def logger
13
+ @@logger
14
+ end
15
+
16
+ def logger=(logger)
17
+ @@logger = logger
18
+ end
19
+ end
20
+
21
+ self.logger = defined?(Rails) ? Rails.logger : nil
22
+
23
+ require 'prawn/markup/processor/text'
24
+ require 'prawn/markup/processor/headings'
25
+ require 'prawn/markup/processor/images'
26
+ require 'prawn/markup/processor/tables'
27
+ require 'prawn/markup/processor/lists'
28
+
29
+ prepend Prawn::Markup::Processor::Text
30
+ prepend Prawn::Markup::Processor::Headings
31
+ prepend Prawn::Markup::Processor::Images
32
+ prepend Prawn::Markup::Processor::Tables
33
+ prepend Prawn::Markup::Processor::Lists
34
+
35
+ def initialize(pdf, options = {})
36
+ @pdf = pdf
37
+ @options = options
38
+ end
39
+
40
+ def parse(html)
41
+ return if html.to_s.strip.empty?
42
+ reset
43
+ html = Prawn::Markup::Normalizer.new(html).normalize
44
+ Nokogiri::HTML::SAX::Parser.new(self).parse(html) { |ctx| ctx.recovery = true }
45
+ end
46
+
47
+ def start_element(name, attrs = [])
48
+ stack.push(name: name, attrs: Hash[attrs])
49
+ if self.class.known_elements.include?(name)
50
+ send("start_#{name}") if respond_to?("start_#{name}", true)
51
+ end
52
+ end
53
+
54
+ def end_element(name)
55
+ send("end_#{name}") if respond_to?("end_#{name}", true)
56
+ stack.pop
57
+ end
58
+
59
+ def characters(string)
60
+ # entities will be replaced again later by inline_format
61
+ append_text(string.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;'))
62
+ end
63
+
64
+ def error(string)
65
+ logger.debug('SAX parsing error: ' + string) if logger
66
+ end
67
+
68
+ def warning(string)
69
+ logger.warn(string) if logger
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :pdf, :stack, :text_buffer, :bottom_margin, :options
75
+
76
+ def reset
77
+ @stack = []
78
+ @text_buffer = ''
79
+ end
80
+
81
+ def append_text(string)
82
+ text_buffer.concat(string)
83
+ end
84
+
85
+ def buffered_text?
86
+ !text_buffer.strip.empty?
87
+ end
88
+
89
+ def dump_text
90
+ text = process_text(text_buffer.dup)
91
+ text_buffer.clear
92
+ text
93
+ end
94
+
95
+ def put_bottom_margin(value)
96
+ @bottom_margin = value
97
+ end
98
+
99
+ def inside_container?
100
+ false
101
+ end
102
+
103
+ def current_attrs
104
+ stack.last[:attrs]
105
+ end
106
+
107
+ def process_text(text)
108
+ if options[:text] && options[:text][:preprocessor]
109
+ options[:text][:preprocessor].call(text)
110
+ else
111
+ text
112
+ end
113
+ end
114
+
115
+ def style_properties
116
+ style = current_attrs['style']
117
+ if style
118
+ tokens = style.split(';').map { |p| p.split(':', 2).map(&:strip) }
119
+ Hash[tokens]
120
+ else
121
+ {}
122
+ end
123
+ end
124
+
125
+ def placeholder_value(keys, *args)
126
+ placeholder = dig_options(keys)
127
+ return if placeholder.nil?
128
+
129
+ if placeholder.respond_to?(:call)
130
+ placeholder.call(*args)
131
+ else
132
+ placeholder.to_s
133
+ end
134
+ end
135
+
136
+ def dig_options(keys)
137
+ keys.inject(options) { |opts, key| opts ? opts[key] : nil }
138
+ end
139
+
140
+ def logger
141
+ self.class.logger
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,64 @@
1
+ module Prawn
2
+ module Markup
3
+ module Processor::Headings
4
+ def self.prepended(base)
5
+ base.known_elements.push('h1', 'h2', 'h3', 'h4', 'h5', 'h6')
6
+ end
7
+
8
+ (1..6).each do |i|
9
+ define_method("start_h#{i}") do
10
+ start_heading(i)
11
+ end
12
+
13
+ define_method("end_h#{i}") do
14
+ end_heading(i)
15
+ end
16
+ end
17
+
18
+ def start_heading(level)
19
+ if current_table
20
+ add_cell_text_node(current_cell)
21
+ elsif current_list
22
+ add_cell_text_node(current_list_item)
23
+ else
24
+ add_current_text if buffered_text?
25
+ pdf.move_down(current_heading_margin_top(level))
26
+ end
27
+ end
28
+
29
+ def end_heading(level)
30
+ options = heading_options(level)
31
+ if current_table
32
+ add_cell_text_node(current_cell, options)
33
+ elsif current_list
34
+ add_cell_text_node(current_list_item, options)
35
+ else
36
+ add_current_text(options)
37
+ put_bottom_margin(options[:margin_bottom])
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def current_heading_margin_top(level)
44
+ top_margin = heading_options(level)[:margin_top] || 0
45
+ margin = [top_margin, @bottom_margin || 0].max
46
+ put_bottom_margin(nil)
47
+ margin
48
+ end
49
+
50
+ def heading_options(level)
51
+ @heading_options ||= {}
52
+ @heading_options[level] ||= default_options_with_size(level)
53
+ end
54
+
55
+ def default_options_with_size(level)
56
+ default = text_options.dup
57
+ default[:size] ||= pdf.font_size
58
+ default[:size] *= 2.5 - level * 0.25
59
+ HashMerger.deep(default, options[:"heading#{level}"] || {})
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,91 @@
1
+ require 'open-uri'
2
+
3
+ module Prawn
4
+ module Markup
5
+ module Processor::Images
6
+ ALLOWED_IMAGE_TYPES = %w[image/png image/jpeg].freeze
7
+
8
+ def self.prepended(base)
9
+ base.known_elements.push('img', 'iframe')
10
+ end
11
+
12
+ def start_img
13
+ add_current_text
14
+ add_image_or_placeholder(current_attrs['src'])
15
+ end
16
+
17
+ def start_iframe
18
+ placeholder = iframe_placeholder
19
+ append_text("\n#{placeholder}\n") if placeholder
20
+ end
21
+
22
+ private
23
+
24
+ def add_image_or_placeholder(src)
25
+ img = image_properties(src)
26
+ if img
27
+ add_image(img)
28
+ else
29
+ append_text("\n#{invalid_image_placeholder}\n")
30
+ end
31
+ end
32
+
33
+ def add_image(img)
34
+ # parse width in the current context
35
+ img[:width] = SizeConverter.new(pdf.bounds.width).parse(style_properties['width'])
36
+ pdf.image(img.delete(:image), img)
37
+ put_bottom_margin(text_margin_bottom)
38
+ rescue Prawn::Errors::UnsupportedImageType
39
+ append_text("\n#{invalid_image_placeholder}\n")
40
+ end
41
+
42
+ def image_properties(src)
43
+ img = load_image(src)
44
+ if img
45
+ props = style_properties
46
+ {
47
+ image: img,
48
+ width: props['width'],
49
+ position: convert_float_to_position(props['float'])
50
+ }
51
+ end
52
+ end
53
+
54
+ def load_image(src)
55
+ if options[:image] && options[:image][:loader]
56
+ options[:image][:loader].call(src)
57
+ else
58
+ decode_base64_image(src) || load_remote_image(src)
59
+ end
60
+ end
61
+
62
+ def decode_base64_image(src)
63
+ match = src.match(/^data:(.*?);(.*?),(.*)$/)
64
+ if match && ALLOWED_IMAGE_TYPES.include?(match[1])
65
+ StringIO.new(Base64.decode64(match[3]))
66
+ end
67
+ end
68
+
69
+ def load_remote_image(src)
70
+ if src =~ %r{^https?:/}
71
+ URI.parse(src).open
72
+ end
73
+ end
74
+
75
+ def convert_float_to_position(float)
76
+ { nil => nil,
77
+ 'none' => nil,
78
+ 'left' => :left,
79
+ 'right' => :right }[float]
80
+ end
81
+
82
+ def invalid_image_placeholder
83
+ placeholder_value(%i[image placeholder]) || '[unsupported image]'
84
+ end
85
+
86
+ def iframe_placeholder
87
+ placeholder_value(%i[iframe placeholder], current_attrs['src'])
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,95 @@
1
+ module Prawn
2
+ module Markup
3
+ module Processor::Lists
4
+ def self.prepended(base)
5
+ base.known_elements.push('ol', 'ul', 'li')
6
+ end
7
+
8
+ def start_ol
9
+ start_list(true)
10
+ end
11
+
12
+ def start_ul
13
+ start_list(false)
14
+ end
15
+
16
+ def start_list(ordered)
17
+ if current_list
18
+ add_cell_text_node(current_list_item)
19
+ elsif current_table
20
+ add_cell_text_node(current_cell)
21
+ else
22
+ add_current_text
23
+ end
24
+ @list_stack.push(Elements::List.new(ordered))
25
+ end
26
+
27
+ def end_list
28
+ list = list_stack.pop
29
+ append_list(list) unless list.items.empty?
30
+ end
31
+ alias end_ol end_list
32
+ alias end_ul end_list
33
+
34
+ def start_li
35
+ current_list.items << Elements::Item.new
36
+ end
37
+
38
+ def end_li
39
+ add_cell_text_node(current_list_item)
40
+ end
41
+
42
+ def start_img
43
+ if current_list
44
+ add_cell_image(current_list_item)
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :list_stack
53
+
54
+ def reset
55
+ @list_stack = []
56
+ super
57
+ end
58
+
59
+ def current_list
60
+ list_stack.last
61
+ end
62
+
63
+ def current_list_item
64
+ current_list.items.last
65
+ end
66
+
67
+ def inside_container?
68
+ super || current_list
69
+ end
70
+
71
+ def append_list(list)
72
+ if list_stack.empty?
73
+ if current_table
74
+ current_cell.nodes << list
75
+ else
76
+ add_list(list)
77
+ end
78
+ else
79
+ current_list_item.nodes << list
80
+ end
81
+ end
82
+
83
+ def add_list(list)
84
+ Builders::ListBuilder.new(pdf, list, pdf.bounds.width, options).draw
85
+ put_bottom_margin(text_margin_bottom)
86
+ rescue Prawn::Errors::CannotFit => e
87
+ append_text(list_too_large_placeholder(e))
88
+ end
89
+
90
+ def list_too_large_placeholder(error)
91
+ placeholder_value(%i[list placeholder too_large], error) || '[list content too large]'
92
+ end
93
+ end
94
+ end
95
+ end