prawn-table-html 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 +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
|