prawn-table-html 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +236 -0
- data/lib/prawn-html.rb +180 -0
- data/lib/prawn_html/attributes.rb +166 -0
- data/lib/prawn_html/callbacks/background.rb +19 -0
- data/lib/prawn_html/callbacks/strike_through.rb +18 -0
- data/lib/prawn_html/context.rb +100 -0
- data/lib/prawn_html/document_renderer.rb +172 -0
- data/lib/prawn_html/html_parser.rb +104 -0
- data/lib/prawn_html/instance.rb +18 -0
- data/lib/prawn_html/pdf_wrapper.rb +145 -0
- data/lib/prawn_html/tag.rb +93 -0
- data/lib/prawn_html/tags/a.rb +20 -0
- data/lib/prawn_html/tags/b.rb +13 -0
- data/lib/prawn_html/tags/blockquote.rb +25 -0
- data/lib/prawn_html/tags/body.rb +13 -0
- data/lib/prawn_html/tags/br.rb +21 -0
- data/lib/prawn_html/tags/code.rb +13 -0
- data/lib/prawn_html/tags/col.rb +37 -0
- data/lib/prawn_html/tags/colgroup.rb +13 -0
- data/lib/prawn_html/tags/del.rb +13 -0
- data/lib/prawn_html/tags/div.rb +13 -0
- data/lib/prawn_html/tags/h.rb +49 -0
- data/lib/prawn_html/tags/hr.rb +39 -0
- data/lib/prawn_html/tags/i.rb +13 -0
- data/lib/prawn_html/tags/img.rb +31 -0
- data/lib/prawn_html/tags/li.rb +39 -0
- data/lib/prawn_html/tags/mark.rb +13 -0
- data/lib/prawn_html/tags/ol.rb +43 -0
- data/lib/prawn_html/tags/p.rb +23 -0
- data/lib/prawn_html/tags/pre.rb +25 -0
- data/lib/prawn_html/tags/small.rb +15 -0
- data/lib/prawn_html/tags/span.rb +9 -0
- data/lib/prawn_html/tags/sub.rb +13 -0
- data/lib/prawn_html/tags/sup.rb +13 -0
- data/lib/prawn_html/tags/table.rb +53 -0
- data/lib/prawn_html/tags/tbody.rb +13 -0
- data/lib/prawn_html/tags/td.rb +43 -0
- data/lib/prawn_html/tags/th.rb +43 -0
- data/lib/prawn_html/tags/tr.rb +37 -0
- data/lib/prawn_html/tags/u.rb +13 -0
- data/lib/prawn_html/tags/ul.rb +40 -0
- data/lib/prawn_html/utils.rb +139 -0
- data/lib/prawn_html/version.rb +5 -0
- metadata +131 -0
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PrawnHtml
|
4
|
+
class Context < Array
|
5
|
+
DEFAULT_STYLES = {
|
6
|
+
size: 16 * PX
|
7
|
+
}.freeze
|
8
|
+
|
9
|
+
attr_reader :previous_tag
|
10
|
+
attr_accessor :last_text_node
|
11
|
+
|
12
|
+
# Init the Context
|
13
|
+
def initialize(*_args)
|
14
|
+
super
|
15
|
+
@last_text_node = false
|
16
|
+
@merged_styles = nil
|
17
|
+
@previous_tag = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
# Add an element to the context
|
21
|
+
#
|
22
|
+
# Set the parent for the previous element in the chain.
|
23
|
+
# Run `on_context_add` callback method on the added element.
|
24
|
+
#
|
25
|
+
# @param element [Tag] the element to add
|
26
|
+
#
|
27
|
+
# @return [Context] the context updated
|
28
|
+
def add(element)
|
29
|
+
element.parent = last
|
30
|
+
push(element)
|
31
|
+
element.on_context_add(self) if element.respond_to?(:on_context_add)
|
32
|
+
@merged_styles = nil
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Evaluate before content
|
37
|
+
#
|
38
|
+
# @return [String] before content string
|
39
|
+
def before_content
|
40
|
+
(last.respond_to?(:before_content) && last.before_content) || ''
|
41
|
+
end
|
42
|
+
|
43
|
+
# Merges the context block styles
|
44
|
+
#
|
45
|
+
# @return [Hash] the hash of merged styles
|
46
|
+
def block_styles
|
47
|
+
each_with_object({}) do |element, res|
|
48
|
+
element.block_styles.each do |key, value|
|
49
|
+
Attributes.merge_attr!(res, key, value)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Merge the context styles for text nodes
|
55
|
+
#
|
56
|
+
# @return [Hash] the hash of merged styles
|
57
|
+
def merged_styles
|
58
|
+
@merged_styles ||=
|
59
|
+
each_with_object(DEFAULT_STYLES.dup) do |element, res|
|
60
|
+
evaluate_element_styles(element, res)
|
61
|
+
element.update_styles(res)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# :nocov:
|
66
|
+
def inspect
|
67
|
+
map(&:class).map(&:to_s).join(', ')
|
68
|
+
end
|
69
|
+
# :nocov:
|
70
|
+
|
71
|
+
# Remove the last element from the context
|
72
|
+
def remove_last
|
73
|
+
last.on_context_remove(self) if last.respond_to?(:on_context_remove)
|
74
|
+
@merged_styles = nil
|
75
|
+
@last_text_node = false
|
76
|
+
@previous_tag = last
|
77
|
+
pop
|
78
|
+
end
|
79
|
+
|
80
|
+
# White space is equal to 'pre'?
|
81
|
+
#
|
82
|
+
# @return [boolean] white space property of the last element is equal to 'pre'
|
83
|
+
def white_space_pre?
|
84
|
+
last && last.styles[:white_space] == :pre
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def evaluate_element_styles(element, res)
|
90
|
+
styles = element.styles.slice(*Attributes::STYLES_APPLY[:text_node])
|
91
|
+
styles.each do |key, val|
|
92
|
+
if res.include?(key) && res[key].is_a?(Array)
|
93
|
+
res[key] += val
|
94
|
+
else
|
95
|
+
res[key] = val
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PrawnHtml
|
4
|
+
class DocumentRenderer
|
5
|
+
NEW_LINE = { text: "\n" }.freeze
|
6
|
+
SPACE = { text: ' ' }.freeze
|
7
|
+
|
8
|
+
# Init the DocumentRenderer
|
9
|
+
#
|
10
|
+
# @param pdf [PdfWrapper] target PDF wrapper
|
11
|
+
def initialize(pdf)
|
12
|
+
@before_content = []
|
13
|
+
@buffer = []
|
14
|
+
@context = Context.new
|
15
|
+
@last_margin = 0
|
16
|
+
@last_text = ''
|
17
|
+
@last_tag_open = false
|
18
|
+
@pdf = pdf
|
19
|
+
end
|
20
|
+
|
21
|
+
# On tag close callback
|
22
|
+
#
|
23
|
+
# @param element [Tag] closing element wrapper
|
24
|
+
def on_tag_close(element)
|
25
|
+
render_if_needed(element)
|
26
|
+
apply_tag_close_styles(element)
|
27
|
+
context.remove_last
|
28
|
+
@last_tag_open = false
|
29
|
+
@last_text = ''
|
30
|
+
end
|
31
|
+
|
32
|
+
# On tag open callback
|
33
|
+
#
|
34
|
+
# @param tag_name [String] the tag name of the opening element
|
35
|
+
# @param attributes [Hash] an hash of the element attributes
|
36
|
+
# @param element_styles [String] document styles to apply to the element
|
37
|
+
#
|
38
|
+
# @return [Tag] the opening element wrapper
|
39
|
+
def on_tag_open(tag_name, attributes:, element_styles: '')
|
40
|
+
tag_class = Tag.class_for(tag_name)
|
41
|
+
return unless tag_class
|
42
|
+
|
43
|
+
options = { width: pdf.page_width, height: pdf.page_height }
|
44
|
+
tag_class.new(tag_name, attributes: attributes, options: options).tap do |element|
|
45
|
+
setup_element(element, element_styles: element_styles)
|
46
|
+
@before_content.push(element.before_content) if element.respond_to?(:before_content)
|
47
|
+
@last_tag_open = true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# On text node callback
|
52
|
+
#
|
53
|
+
# @param content [String] the text node content
|
54
|
+
#
|
55
|
+
# @return [NilClass] nil value (=> no element)
|
56
|
+
def on_text_node(content)
|
57
|
+
if context.last.respond_to?(:on_text_node)
|
58
|
+
# Avoid geting text from table rendered before it.
|
59
|
+
context.last.on_text_node(content)
|
60
|
+
return
|
61
|
+
end
|
62
|
+
|
63
|
+
return if context.previous_tag&.block? && content.match?(/\A\s*\Z/)
|
64
|
+
|
65
|
+
text = prepare_text(content)
|
66
|
+
buffer << context.merged_styles.merge(text: text) unless text.empty?
|
67
|
+
context.last_text_node = true
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
# Render the buffer content to the PDF document
|
72
|
+
def render
|
73
|
+
return if buffer.empty?
|
74
|
+
|
75
|
+
output_content(buffer.dup, context.block_styles)
|
76
|
+
buffer.clear
|
77
|
+
@last_margin = 0
|
78
|
+
end
|
79
|
+
|
80
|
+
alias_method :flush, :render
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
attr_reader :buffer, :context, :last_margin, :pdf
|
85
|
+
|
86
|
+
def setup_element(element, element_styles:)
|
87
|
+
render_if_needed(element)
|
88
|
+
context.add(element)
|
89
|
+
element.process_styles(element_styles: element_styles)
|
90
|
+
apply_tag_open_styles(element)
|
91
|
+
element.custom_render(pdf, context) if element.respond_to?(:custom_render)
|
92
|
+
end
|
93
|
+
|
94
|
+
def render_if_needed(element)
|
95
|
+
render_needed = element&.block? && buffer.any? && buffer.last != NEW_LINE
|
96
|
+
return false unless render_needed
|
97
|
+
|
98
|
+
render
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
def apply_tag_close_styles(element)
|
103
|
+
tag_styles = element.tag_close_styles
|
104
|
+
@last_margin = tag_styles[:margin_bottom].to_f
|
105
|
+
puts "apply_tag_close_styles(#{element.tag}), margin_bottom=#{tag_styles[:margin_bottom]}, last_margin=#{@last_margin}"
|
106
|
+
pdf.advance_cursor(last_margin + tag_styles[:padding_bottom].to_f)
|
107
|
+
pdf.start_new_page if tag_styles[:break_after]
|
108
|
+
end
|
109
|
+
|
110
|
+
def apply_tag_open_styles(element)
|
111
|
+
tag_styles = element.tag_open_styles
|
112
|
+
move_down = (tag_styles[:margin_top].to_f - last_margin) + tag_styles[:padding_top].to_f
|
113
|
+
puts "apply_tag_open_styles(#{element.tag}), margin_top=#{tag_styles[:margin_top]}, padding_top: #{tag_styles[:padding_top]} last_margin=#{@last_margin}"
|
114
|
+
pdf.advance_cursor(move_down) if move_down > 0
|
115
|
+
pdf.start_new_page if tag_styles[:break_before]
|
116
|
+
end
|
117
|
+
|
118
|
+
def prepare_text(content)
|
119
|
+
text = @before_content.any? ? ::Oga::HTML::Entities.decode(@before_content.join) : ''
|
120
|
+
@before_content.clear
|
121
|
+
|
122
|
+
return (@last_text = text + content) if context.white_space_pre?
|
123
|
+
|
124
|
+
content = content.lstrip if @last_text[-1] == ' ' || @last_tag_open
|
125
|
+
text += content.tr("\n", ' ').squeeze(' ')
|
126
|
+
@last_text = text
|
127
|
+
end
|
128
|
+
|
129
|
+
def output_content(buffer, block_styles)
|
130
|
+
apply_callbacks(buffer)
|
131
|
+
left_indent = block_styles[:margin_left].to_f + block_styles[:padding_left].to_f
|
132
|
+
options = block_styles.slice(:align, :indent_paragraphs, :leading, :mode, :padding_left)
|
133
|
+
options[:leading] = adjust_leading(buffer, options[:leading])
|
134
|
+
pdf.puts(buffer, options, bounding_box: bounds(buffer, options, block_styles), left_indent: left_indent)
|
135
|
+
end
|
136
|
+
|
137
|
+
def apply_callbacks(buffer)
|
138
|
+
buffer.select { |item| item[:callback] }.each do |item|
|
139
|
+
callback, arg = item[:callback]
|
140
|
+
callback_class = Tag::CALLBACKS[callback]
|
141
|
+
item[:callback] = callback_class.new(pdf, arg)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def adjust_leading(buffer, leading)
|
146
|
+
return leading if leading
|
147
|
+
|
148
|
+
leadings = buffer.map do |item|
|
149
|
+
(item[:size] || Context::DEFAULT_STYLES[:size]) * (ADJUST_LEADING[item[:font]] || ADJUST_LEADING[nil])
|
150
|
+
end
|
151
|
+
leadings.max.round(4)
|
152
|
+
end
|
153
|
+
|
154
|
+
def bounds(buffer, options, block_styles)
|
155
|
+
return unless block_styles[:position] == :absolute
|
156
|
+
|
157
|
+
x = if block_styles.include?(:right)
|
158
|
+
x1 = pdf.calc_buffer_width(buffer) + block_styles[:right]
|
159
|
+
x1 < pdf.page_width ? (pdf.page_width - x1) : 0
|
160
|
+
else
|
161
|
+
block_styles[:left] || 0
|
162
|
+
end
|
163
|
+
y = if block_styles.include?(:bottom)
|
164
|
+
pdf.calc_buffer_height(buffer, options) + block_styles[:bottom]
|
165
|
+
else
|
166
|
+
pdf.page_height - (block_styles[:top] || 0)
|
167
|
+
end
|
168
|
+
|
169
|
+
[[x, y], { width: pdf.page_width - x }]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'oga'
|
4
|
+
|
5
|
+
module PrawnHtml
|
6
|
+
class HtmlParser
|
7
|
+
REGEXP_STYLES = /\s*([^{\s]+)\s*{\s*([^}]*?)\s*}/m.freeze
|
8
|
+
|
9
|
+
# Init the HtmlParser
|
10
|
+
#
|
11
|
+
# @param renderer [DocumentRenderer] document renderer
|
12
|
+
# @param ignore_content_tags [Array] array of tags (symbols) to skip their contents while preparing the PDF document
|
13
|
+
def initialize(renderer, ignore_content_tags: %i[script style])
|
14
|
+
@processing = false
|
15
|
+
@ignore = false
|
16
|
+
@ignore_content_tags = ignore_content_tags
|
17
|
+
@renderer = renderer
|
18
|
+
@raw_styles = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Processes HTML and renders it
|
22
|
+
#
|
23
|
+
# @param html [String] The HTML content to process
|
24
|
+
def process(html)
|
25
|
+
@styles = {}
|
26
|
+
@processing = !html.include?('<body')
|
27
|
+
@document = Oga.parse_html(html)
|
28
|
+
process_styles # apply previously loaded styles
|
29
|
+
traverse_nodes(document.children)
|
30
|
+
renderer.flush
|
31
|
+
end
|
32
|
+
|
33
|
+
# Parses CSS styles
|
34
|
+
#
|
35
|
+
# @param text_styles [String] The CSS styles to evaluate
|
36
|
+
def parse_styles(text_styles)
|
37
|
+
@raw_styles = text_styles.scan(REGEXP_STYLES).to_h
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :document, :ignore, :processing, :renderer, :styles
|
43
|
+
|
44
|
+
def traverse_nodes(nodes)
|
45
|
+
nodes.each do |node|
|
46
|
+
next if node.is_a?(Oga::XML::Comment)
|
47
|
+
|
48
|
+
element = node_open(node)
|
49
|
+
traverse_nodes(node.children) if node.children.any?
|
50
|
+
node_close(element) if element
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def node_open(node)
|
55
|
+
tag = node.is_a?(Oga::XML::Element) && init_element(node)
|
56
|
+
return unless processing
|
57
|
+
return IgnoredTag.new(tag) if ignore
|
58
|
+
return renderer.on_text_node(node.text) unless tag
|
59
|
+
|
60
|
+
renderer.on_tag_open(tag, attributes: prepare_attributes(node), element_styles: styles[node])
|
61
|
+
end
|
62
|
+
|
63
|
+
def init_element(node)
|
64
|
+
node.name.downcase.to_sym.tap do |tag_name|
|
65
|
+
@processing = true if tag_name == :body
|
66
|
+
@ignore = true if @processing && @ignore_content_tags.include?(tag_name)
|
67
|
+
process_styles(node.text) if tag_name == :style
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def process_styles(text_styles = nil)
|
72
|
+
parse_styles(text_styles) if text_styles
|
73
|
+
@raw_styles.each do |selector, rule|
|
74
|
+
document.css(selector).each do |node|
|
75
|
+
styles[node] = rule
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def prepare_attributes(node)
|
81
|
+
node.attributes.each_with_object({}) do |attr, res|
|
82
|
+
res[attr.name] = attr.value
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def node_close(element)
|
87
|
+
if processing
|
88
|
+
renderer.on_tag_close(element) unless ignore
|
89
|
+
@ignore = false if ignore && @ignore_content_tags.include?(element.tag)
|
90
|
+
end
|
91
|
+
@processing = false if element.tag == :body
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class IgnoredTag
|
96
|
+
attr_accessor :tag
|
97
|
+
|
98
|
+
def initialize(tag_name)
|
99
|
+
@tag = tag_name
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
HtmlHandler = HtmlParser
|
104
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PrawnHtml
|
4
|
+
class Instance
|
5
|
+
attr_reader :html_parser, :pdf_wrapper, :renderer
|
6
|
+
|
7
|
+
def initialize(pdf)
|
8
|
+
@pdf_wrapper = PrawnHtml::PdfWrapper.new(pdf)
|
9
|
+
@renderer = PrawnHtml::DocumentRenderer.new(@pdf_wrapper)
|
10
|
+
@html_parser = PrawnHtml::HtmlParser.new(@renderer)
|
11
|
+
end
|
12
|
+
|
13
|
+
def append(css: nil, html: nil)
|
14
|
+
html_parser.parse_styles(css) if css
|
15
|
+
html_parser.process(html) if html
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module PrawnHtml
|
6
|
+
class PdfWrapper
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :@pdf, :start_new_page
|
10
|
+
|
11
|
+
# Wrapper for Prawn PDF Document
|
12
|
+
#
|
13
|
+
# @param pdf_document [Prawn::Document] PDF document to wrap
|
14
|
+
def initialize(pdf_document)
|
15
|
+
@pdf = pdf_document
|
16
|
+
end
|
17
|
+
|
18
|
+
# Advance the cursor
|
19
|
+
#
|
20
|
+
# @param move_down [Float] Quantity to advance (move down)
|
21
|
+
def advance_cursor(move_down)
|
22
|
+
return if !move_down || move_down == 0
|
23
|
+
|
24
|
+
pdf.move_down(move_down)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Calculate the height of a buffer of items
|
28
|
+
#
|
29
|
+
# @param buffer [Array] Buffer of items
|
30
|
+
# @param options [Hash] Output options
|
31
|
+
#
|
32
|
+
# @return [Float] calculated height
|
33
|
+
def calc_buffer_height(buffer, options)
|
34
|
+
pdf.height_of_formatted(buffer, options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Calculate the width of a buffer of items
|
38
|
+
#
|
39
|
+
# @param buffer [Array] Buffer of items
|
40
|
+
#
|
41
|
+
# @return [Float] calculated width
|
42
|
+
def calc_buffer_width(buffer)
|
43
|
+
width = 0
|
44
|
+
buffer.each do |item|
|
45
|
+
font_family = item[:font] || pdf.font.name
|
46
|
+
pdf.font(font_family, size: item[:size] || pdf.font_size) do
|
47
|
+
width += pdf.width_of(item[:text], inline_format: true)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
width
|
51
|
+
end
|
52
|
+
|
53
|
+
# Height of the page
|
54
|
+
#
|
55
|
+
# @return [Float] height
|
56
|
+
def page_height
|
57
|
+
pdf.bounds.height
|
58
|
+
end
|
59
|
+
|
60
|
+
# Width of the page
|
61
|
+
#
|
62
|
+
# @return [Float] width
|
63
|
+
def page_width
|
64
|
+
pdf.bounds.width
|
65
|
+
end
|
66
|
+
|
67
|
+
# Draw a rectangle
|
68
|
+
#
|
69
|
+
# @param x [Float] left position of the rectangle
|
70
|
+
# @param y [Float] top position of the rectangle
|
71
|
+
# @param width [Float] width of the rectangle
|
72
|
+
# @param height [Float] height of the rectangle
|
73
|
+
# @param color [String] fill color
|
74
|
+
def draw_rectangle(x:, y:, width:, height:, color:)
|
75
|
+
current_fill_color = pdf.fill_color
|
76
|
+
pdf.fill_color = color
|
77
|
+
pdf.fill_rectangle([y, x], width, height)
|
78
|
+
pdf.fill_color = current_fill_color
|
79
|
+
end
|
80
|
+
|
81
|
+
# Horizontal line
|
82
|
+
#
|
83
|
+
# @param color [String] line color
|
84
|
+
# @param dash [Integer|Array] integer or array of integer with dash options
|
85
|
+
def horizontal_rule(color:, dash:)
|
86
|
+
current_color = pdf.stroke_color
|
87
|
+
pdf.dash(dash) if dash
|
88
|
+
pdf.stroke_color = color if color
|
89
|
+
pdf.stroke_horizontal_rule
|
90
|
+
pdf.stroke_color = current_color if color
|
91
|
+
pdf.undash if dash
|
92
|
+
end
|
93
|
+
|
94
|
+
# Image
|
95
|
+
#
|
96
|
+
# @param src [String] image source path
|
97
|
+
# @param options [Hash] hash of options
|
98
|
+
def image(src, options = {})
|
99
|
+
return unless src
|
100
|
+
|
101
|
+
pdf.image(src, options)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Output to the PDF document
|
105
|
+
#
|
106
|
+
# @param buffer [Array] array of text items
|
107
|
+
# @param options [Hash] hash of options
|
108
|
+
# @param bounding_box [Array] bounding box arguments, if bounded
|
109
|
+
def puts(buffer, options, bounding_box: nil, left_indent: 0)
|
110
|
+
return output_buffer(buffer, options, left_indent: left_indent) unless bounding_box
|
111
|
+
|
112
|
+
current_y = pdf.cursor
|
113
|
+
pdf.bounding_box(*bounding_box) do
|
114
|
+
output_buffer(buffer, options, left_indent: left_indent)
|
115
|
+
end
|
116
|
+
pdf.move_cursor_to(current_y)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Underline
|
120
|
+
#
|
121
|
+
# @param x1 [Float] left position of the line
|
122
|
+
# @param x2 [Float] right position of the line
|
123
|
+
# @param y [Float] vertical position of the line
|
124
|
+
def underline(x1:, x2:, y:)
|
125
|
+
pdf.stroke do
|
126
|
+
pdf.line [x1, y], [x2, y]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def table(rows, options = {})
|
131
|
+
pdf.table(rows, options) # uses prawn-table
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
attr_reader :pdf
|
137
|
+
|
138
|
+
def output_buffer(buffer, options, left_indent:)
|
139
|
+
formatted_text = proc { pdf.formatted_text(buffer, options) }
|
140
|
+
return formatted_text.call if left_indent == 0
|
141
|
+
|
142
|
+
pdf.indent(left_indent, 0, &formatted_text)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PrawnHtml
|
4
|
+
class Tag
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
CALLBACKS = {
|
8
|
+
'Background' => Callbacks::Background,
|
9
|
+
'StrikeThrough' => Callbacks::StrikeThrough
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
TAG_CLASSES = %w[
|
13
|
+
A B Blockquote Body Br Code Del Div H Hr I Img Li Mark Ol P Pre Small Span Sub Sup U Ul
|
14
|
+
Table Tr Td Th Tbody Colgroup Col
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
def_delegators :@attrs, :styles, :update_styles
|
18
|
+
|
19
|
+
attr_accessor :parent
|
20
|
+
attr_reader :attrs, :tag
|
21
|
+
|
22
|
+
# Init the Tag
|
23
|
+
#
|
24
|
+
# @param tag [Symbol] tag name
|
25
|
+
# @param attributes [Hash] hash of element attributes
|
26
|
+
# @param options [Hash] options (container width/height/etc.)
|
27
|
+
def initialize(tag, attributes: {}, options: {})
|
28
|
+
@tag = tag
|
29
|
+
@options = options
|
30
|
+
@attrs = Attributes.new(attributes)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Is a block tag?
|
34
|
+
#
|
35
|
+
# @return [Boolean] true if the type of the tag is block, false otherwise
|
36
|
+
def block?
|
37
|
+
false
|
38
|
+
end
|
39
|
+
|
40
|
+
# Styles to apply to the block
|
41
|
+
#
|
42
|
+
# @return [Hash] hash of styles to apply
|
43
|
+
def block_styles
|
44
|
+
block_styles = styles.slice(*Attributes::STYLES_APPLY[:block])
|
45
|
+
block_styles[:mode] = attrs.data['mode'].to_sym if attrs.data.include?('mode')
|
46
|
+
block_styles
|
47
|
+
end
|
48
|
+
|
49
|
+
# Process tag styles
|
50
|
+
#
|
51
|
+
# @param element_styles [String] extra styles to apply to the element
|
52
|
+
def process_styles(element_styles: nil)
|
53
|
+
attrs.merge_text_styles!(tag_styles, options: options) if respond_to?(:tag_styles)
|
54
|
+
attrs.merge_text_styles!(element_styles, options: options) if element_styles
|
55
|
+
attrs.merge_text_styles!(attrs.style, options: options)
|
56
|
+
attrs.merge_text_styles!(extra_styles, options: options) if respond_to?(:extra_styles)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Styles to apply on tag closing
|
60
|
+
#
|
61
|
+
# @return [Hash] hash of styles to apply
|
62
|
+
def tag_close_styles
|
63
|
+
styles.slice(*Attributes::STYLES_APPLY[:tag_close])
|
64
|
+
end
|
65
|
+
|
66
|
+
# Styles to apply on tag opening
|
67
|
+
#
|
68
|
+
# @return [Hash] hash of styles to apply
|
69
|
+
def tag_open_styles
|
70
|
+
styles.slice(*Attributes::STYLES_APPLY[:tag_open])
|
71
|
+
end
|
72
|
+
|
73
|
+
class << self
|
74
|
+
# Evaluate the Tag class from a tag name
|
75
|
+
#
|
76
|
+
# @params tag_name [Symbol] the tag name
|
77
|
+
#
|
78
|
+
# @return [Tag] the class for the tag if available or nil
|
79
|
+
def class_for(tag_name)
|
80
|
+
@tag_classes ||= TAG_CLASSES.each_with_object({}) do |tag_class, res|
|
81
|
+
klass = const_get("PrawnHtml::Tags::#{tag_class}")
|
82
|
+
k = [klass] * klass::ELEMENTS.size
|
83
|
+
res.merge!(klass::ELEMENTS.zip(k).to_h)
|
84
|
+
end
|
85
|
+
@tag_classes[tag_name]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
attr_reader :options
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PrawnHtml
|
4
|
+
module Tags
|
5
|
+
class A < Tag
|
6
|
+
ELEMENTS = [:a].freeze
|
7
|
+
|
8
|
+
def extra_styles
|
9
|
+
attrs.href ? "href: #{attrs.href}" : nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def tag_styles
|
13
|
+
<<~STYLES
|
14
|
+
color: #00E;
|
15
|
+
text-decoration: underline;
|
16
|
+
STYLES
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PrawnHtml
|
4
|
+
module Tags
|
5
|
+
class Blockquote < Tag
|
6
|
+
ELEMENTS = [:blockquote].freeze
|
7
|
+
|
8
|
+
MARGIN_BOTTOM = 12.7
|
9
|
+
MARGIN_LEFT = 40.4
|
10
|
+
MARGIN_TOP = 12.7
|
11
|
+
|
12
|
+
def block?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def tag_styles
|
17
|
+
<<~STYLES
|
18
|
+
margin-bottom: #{MARGIN_BOTTOM}px;
|
19
|
+
margin-left: #{MARGIN_LEFT}px;
|
20
|
+
margin-top: #{MARGIN_TOP}px;
|
21
|
+
STYLES
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|