prawn-html 0.2.0 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +45 -6
- data/lib/prawn-html.rb +170 -15
- data/lib/prawn_html/attributes.rb +33 -101
- data/lib/prawn_html/callbacks/highlight.rb +2 -4
- data/lib/prawn_html/callbacks/strike_through.rb +4 -4
- data/lib/prawn_html/context.rb +16 -3
- data/lib/prawn_html/document_renderer.rb +39 -27
- data/lib/prawn_html/html_parser.rb +95 -0
- data/lib/prawn_html/pdf_wrapper.rb +94 -0
- data/lib/prawn_html/tag.rb +13 -22
- data/lib/prawn_html/tags/a.rb +1 -1
- data/lib/prawn_html/tags/b.rb +1 -3
- data/lib/prawn_html/tags/blockquote.rb +25 -0
- data/lib/prawn_html/tags/br.rb +2 -3
- data/lib/prawn_html/tags/code.rb +13 -0
- data/lib/prawn_html/tags/del.rb +1 -3
- data/lib/prawn_html/tags/h.rb +6 -6
- data/lib/prawn_html/tags/hr.rb +6 -8
- data/lib/prawn_html/tags/i.rb +1 -3
- data/lib/prawn_html/tags/img.rb +7 -7
- data/lib/prawn_html/tags/li.rb +7 -4
- data/lib/prawn_html/tags/mark.rb +1 -3
- data/lib/prawn_html/tags/ol.rb +26 -0
- data/lib/prawn_html/tags/p.rb +4 -4
- data/lib/prawn_html/tags/pre.rb +25 -0
- data/lib/prawn_html/tags/sub.rb +13 -0
- data/lib/prawn_html/tags/sup.rb +13 -0
- data/lib/prawn_html/tags/u.rb +1 -3
- data/lib/prawn_html/tags/ul.rb +1 -3
- data/lib/prawn_html/utils.rb +109 -0
- data/lib/prawn_html/version.rb +1 -1
- metadata +11 -3
- data/lib/prawn_html/html_handler.rb +0 -66
@@ -11,10 +11,8 @@ module PrawnHtml
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def render_behind(fragment)
|
14
|
-
|
15
|
-
@pdf.
|
16
|
-
@pdf.fill_rectangle(fragment.top_left, fragment.width, fragment.height)
|
17
|
-
@pdf.fill_color = original_color
|
14
|
+
top, left = fragment.top_left
|
15
|
+
@pdf.draw_rectangle(x: left, y: top, width: fragment.width, height: fragment.height, color: @color)
|
18
16
|
end
|
19
17
|
end
|
20
18
|
end
|
@@ -8,10 +8,10 @@ module PrawnHtml
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def render_in_front(fragment)
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
x1 = fragment.left
|
12
|
+
x2 = fragment.right
|
13
|
+
y = (fragment.top + fragment.bottom) / 2
|
14
|
+
@pdf.underline(x1: x1, x2: x2, y: y)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
data/lib/prawn_html/context.rb
CHANGED
@@ -13,13 +13,26 @@ module PrawnHtml
|
|
13
13
|
@last_text_node = false
|
14
14
|
end
|
15
15
|
|
16
|
+
# Add an element to the context
|
17
|
+
#
|
18
|
+
# Set the parent for the previous element in the chain.
|
19
|
+
# Run `on_context_add` callback method on the added element.
|
20
|
+
#
|
21
|
+
# @param element [Tag] the element to add
|
22
|
+
#
|
23
|
+
# @return [Context] the context updated
|
24
|
+
def add(element)
|
25
|
+
element.parent = last
|
26
|
+
push(element)
|
27
|
+
element.on_context_add(self) if element.respond_to?(:on_context_add)
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
16
31
|
# Evaluate before content
|
17
32
|
#
|
18
33
|
# @return [String] before content string
|
19
34
|
def before_content
|
20
|
-
|
21
|
-
|
22
|
-
last.tag_styles[:before_content].to_s
|
35
|
+
(last.respond_to?(:before_content) && last.before_content) || ''
|
23
36
|
end
|
24
37
|
|
25
38
|
# Merges the context block styles
|
@@ -7,23 +7,13 @@ module PrawnHtml
|
|
7
7
|
|
8
8
|
# Init the DocumentRenderer
|
9
9
|
#
|
10
|
-
# @param pdf [
|
10
|
+
# @param pdf [PdfWrapper] target PDF wrapper
|
11
11
|
def initialize(pdf)
|
12
12
|
@buffer = []
|
13
13
|
@context = Context.new
|
14
|
-
@document_styles = {}
|
15
14
|
@pdf = pdf
|
16
15
|
end
|
17
16
|
|
18
|
-
# Evaluate the document styles and store the internally
|
19
|
-
#
|
20
|
-
# @param styles [Hash] styles hash with CSS selectors as keys and rules as values
|
21
|
-
def assign_document_styles(styles)
|
22
|
-
@document_styles = styles.transform_values do |style_rules|
|
23
|
-
Attributes.new(style: style_rules).styles
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
17
|
# On tag close callback
|
28
18
|
#
|
29
19
|
# @param element [Tag] closing element wrapper
|
@@ -38,13 +28,14 @@ module PrawnHtml
|
|
38
28
|
#
|
39
29
|
# @param tag_name [String] the tag name of the opening element
|
40
30
|
# @param attributes [Hash] an hash of the element attributes
|
31
|
+
# @param element_styles [String] document styles to apply to the element
|
41
32
|
#
|
42
33
|
# @return [Tag] the opening element wrapper
|
43
|
-
def on_tag_open(tag_name, attributes)
|
34
|
+
def on_tag_open(tag_name, attributes:, element_styles: '')
|
44
35
|
tag_class = Tag.class_for(tag_name)
|
45
36
|
return unless tag_class
|
46
37
|
|
47
|
-
tag_class.new(tag_name, attributes,
|
38
|
+
tag_class.new(tag_name, attributes: attributes, element_styles: element_styles).tap do |element|
|
48
39
|
setup_element(element)
|
49
40
|
end
|
50
41
|
end
|
@@ -57,9 +48,7 @@ module PrawnHtml
|
|
57
48
|
def on_text_node(content)
|
58
49
|
return if content.match?(/\A\s*\Z/)
|
59
50
|
|
60
|
-
|
61
|
-
text += content.gsub(/\A\s*\n\s*|\s*\n\s*\Z/, '').delete("\n").squeeze(' ')
|
62
|
-
buffer << context.text_node_styles.merge(text: text)
|
51
|
+
buffer << context.text_node_styles.merge(text: prepare_text(content))
|
63
52
|
context.last_text_node = true
|
64
53
|
nil
|
65
54
|
end
|
@@ -68,8 +57,7 @@ module PrawnHtml
|
|
68
57
|
def render
|
69
58
|
return if buffer.empty?
|
70
59
|
|
71
|
-
|
72
|
-
output_content(buffer.dup, options)
|
60
|
+
output_content(buffer.dup, context.block_styles)
|
73
61
|
buffer.clear
|
74
62
|
context.last_margin = 0
|
75
63
|
end
|
@@ -78,12 +66,12 @@ module PrawnHtml
|
|
78
66
|
|
79
67
|
private
|
80
68
|
|
81
|
-
attr_reader :buffer, :context, :
|
69
|
+
attr_reader :buffer, :context, :pdf
|
82
70
|
|
83
71
|
def setup_element(element)
|
84
72
|
add_space_if_needed unless render_if_needed(element)
|
85
73
|
apply_tag_open_styles(element)
|
86
|
-
context.
|
74
|
+
context.add(element)
|
87
75
|
element.custom_render(pdf, context) if element.respond_to?(:custom_render)
|
88
76
|
end
|
89
77
|
|
@@ -102,21 +90,45 @@ module PrawnHtml
|
|
102
90
|
def apply_tag_close_styles(element)
|
103
91
|
tag_styles = element.tag_close_styles
|
104
92
|
context.last_margin = tag_styles[:margin_bottom].to_f
|
105
|
-
|
106
|
-
pdf.
|
93
|
+
pdf.advance_cursor(context.last_margin + tag_styles[:padding_bottom].to_f)
|
94
|
+
pdf.start_new_page if tag_styles[:break_after]
|
107
95
|
end
|
108
96
|
|
109
97
|
def apply_tag_open_styles(element)
|
110
98
|
tag_styles = element.tag_open_styles
|
111
99
|
move_down = (tag_styles[:margin_top].to_f - context.last_margin) + tag_styles[:padding_top].to_f
|
112
|
-
pdf.
|
100
|
+
pdf.advance_cursor(move_down) if move_down > 0
|
101
|
+
pdf.start_new_page if tag_styles[:break_before]
|
102
|
+
end
|
103
|
+
|
104
|
+
def prepare_text(content)
|
105
|
+
white_space_pre = context.last && context.last.styles[:white_space] == :pre
|
106
|
+
text = ::Oga::HTML::Entities.decode(context.before_content)
|
107
|
+
text += white_space_pre ? content : content.gsub(/\A\s*\n\s*|\s*\n\s*\Z/, '').delete("\n").squeeze(' ')
|
108
|
+
text
|
113
109
|
end
|
114
110
|
|
115
|
-
def output_content(buffer,
|
116
|
-
buffer
|
117
|
-
left_indent =
|
111
|
+
def output_content(buffer, block_styles)
|
112
|
+
apply_callbacks(buffer)
|
113
|
+
left_indent = block_styles[:margin_left].to_f + block_styles[:padding_left].to_f
|
114
|
+
options = block_styles.slice(:align, :leading, :mode, :padding_left)
|
118
115
|
options[:indent_paragraphs] = left_indent if left_indent > 0
|
119
|
-
pdf.
|
116
|
+
pdf.puts(buffer, options, bounding_box: bounds(block_styles))
|
117
|
+
end
|
118
|
+
|
119
|
+
def apply_callbacks(buffer)
|
120
|
+
buffer.select { |item| item[:callback] }.each do |item|
|
121
|
+
callback = Tag::CALLBACKS[item[:callback]]
|
122
|
+
item[:callback] = callback.new(pdf, item)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def bounds(block_styles)
|
127
|
+
return unless block_styles[:position] == :absolute
|
128
|
+
|
129
|
+
y = pdf.bounds.height - (block_styles[:top] || 0)
|
130
|
+
w = pdf.bounds.width - (block_styles[:left] || 0)
|
131
|
+
[[block_styles[:left] || 0, y], { width: w }]
|
120
132
|
end
|
121
133
|
end
|
122
134
|
end
|
@@ -0,0 +1,95 @@
|
|
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
|
+
@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
|
+
@processing = !html.include?('<body')
|
26
|
+
@document = Oga.parse_html(html)
|
27
|
+
traverse_nodes(document.children)
|
28
|
+
renderer.flush
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :document, :ignore, :processing, :renderer, :styles
|
34
|
+
|
35
|
+
def traverse_nodes(nodes)
|
36
|
+
nodes.each do |node|
|
37
|
+
next if node.is_a?(Oga::XML::Comment)
|
38
|
+
|
39
|
+
element = node_open(node)
|
40
|
+
traverse_nodes(node.children) if node.children.any?
|
41
|
+
node_close(element) if element
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def node_open(node)
|
46
|
+
tag = node.is_a?(Oga::XML::Element) && init_element(node)
|
47
|
+
return unless processing
|
48
|
+
return IgnoredTag.new(tag) if ignore
|
49
|
+
return renderer.on_text_node(node.text) unless tag
|
50
|
+
|
51
|
+
renderer.on_tag_open(tag, attributes: prepare_attributes(node), element_styles: styles[node])
|
52
|
+
end
|
53
|
+
|
54
|
+
def init_element(node)
|
55
|
+
node.name.downcase.to_sym.tap do |tag_name|
|
56
|
+
@processing = true if tag_name == :body
|
57
|
+
@ignore = true if @processing && @ignore_content_tags.include?(tag_name)
|
58
|
+
process_styles(node.text) if tag_name == :style
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def process_styles(text_styles)
|
63
|
+
hash_styles = text_styles.scan(REGEXP_STYLES).to_h
|
64
|
+
hash_styles.each do |selector, rule|
|
65
|
+
document.css(selector).each do |node|
|
66
|
+
styles[node] = rule
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def prepare_attributes(node)
|
72
|
+
node.attributes.each_with_object({}) do |attr, res|
|
73
|
+
res[attr.name] = attr.value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def node_close(element)
|
78
|
+
if processing
|
79
|
+
renderer.on_tag_close(element) unless ignore
|
80
|
+
@ignore = false if ignore && @ignore_content_tags.include?(element.tag)
|
81
|
+
end
|
82
|
+
@processing = false if element.tag == :body
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class IgnoredTag
|
87
|
+
attr_accessor :tag
|
88
|
+
|
89
|
+
def initialize(tag_name)
|
90
|
+
@tag = tag_name
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
HtmlHandler = HtmlParser
|
95
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module PrawnHtml
|
6
|
+
class PdfWrapper
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :@pdf, :bounds, :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
|
+
# Draw a rectangle
|
28
|
+
#
|
29
|
+
# @param x [Float] left position of the rectangle
|
30
|
+
# @param y [Float] top position of the rectangle
|
31
|
+
# @param width [Float] width of the rectangle
|
32
|
+
# @param height [Float] height of the rectangle
|
33
|
+
# @param color [String] fill color
|
34
|
+
def draw_rectangle(x:, y:, width:, height:, color:)
|
35
|
+
current_fill_color = pdf.fill_color
|
36
|
+
pdf.fill_color = color
|
37
|
+
pdf.fill_rectangle([y, x], width, height)
|
38
|
+
pdf.fill_color = current_fill_color
|
39
|
+
end
|
40
|
+
|
41
|
+
# Horizontal line
|
42
|
+
#
|
43
|
+
# @param color [String] line color
|
44
|
+
# @param dash [Integer|Array] integer or array of integer with dash options
|
45
|
+
def horizontal_rule(color:, dash:)
|
46
|
+
current_color = pdf.stroke_color
|
47
|
+
pdf.dash(dash) if dash
|
48
|
+
pdf.stroke_color = color if color
|
49
|
+
pdf.stroke_horizontal_rule
|
50
|
+
pdf.stroke_color = current_color if color
|
51
|
+
pdf.undash if dash
|
52
|
+
end
|
53
|
+
|
54
|
+
# Image
|
55
|
+
#
|
56
|
+
# @param src [String] image source path
|
57
|
+
# @param options [Hash] hash of options
|
58
|
+
def image(src, options = {})
|
59
|
+
return unless src
|
60
|
+
|
61
|
+
pdf.image(src, options)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Output to the PDF document
|
65
|
+
#
|
66
|
+
# @param buffer [Array] array of text items
|
67
|
+
# @param options [Hash] hash of options
|
68
|
+
# @param bounding_box [Array] bounding box arguments, if bounded
|
69
|
+
def puts(buffer, options, bounding_box: nil)
|
70
|
+
return pdf.formatted_text(buffer, options) unless bounding_box
|
71
|
+
|
72
|
+
current_y = pdf.cursor
|
73
|
+
pdf.bounding_box(*bounding_box) do
|
74
|
+
pdf.formatted_text(buffer, options)
|
75
|
+
end
|
76
|
+
pdf.move_cursor_to(current_y)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Underline
|
80
|
+
#
|
81
|
+
# @param x1 [Float] left position of the line
|
82
|
+
# @param x2 [Float] right position of the line
|
83
|
+
# @param y [Float] vertical position of the line
|
84
|
+
def underline(x1:, x2:, y:)
|
85
|
+
pdf.stroke do
|
86
|
+
pdf.line [x1, y], [x2, y]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
attr_reader :pdf
|
93
|
+
end
|
94
|
+
end
|
data/lib/prawn_html/tag.rb
CHANGED
@@ -2,20 +2,24 @@
|
|
2
2
|
|
3
3
|
module PrawnHtml
|
4
4
|
class Tag
|
5
|
-
|
5
|
+
CALLBACKS = {
|
6
|
+
'Highlight' => Callbacks::Highlight,
|
7
|
+
'StrikeThrough' => Callbacks::StrikeThrough
|
8
|
+
}.freeze
|
9
|
+
TAG_CLASSES = %w[A B Blockquote Body Br Code Del Div H Hr I Img Li Mark Ol P Pre Small Span Sub Sup U Ul].freeze
|
6
10
|
|
11
|
+
attr_accessor :parent
|
7
12
|
attr_reader :attrs, :tag
|
8
13
|
|
9
14
|
# Init the Tag
|
10
15
|
#
|
11
16
|
# @param tag [Symbol] tag name
|
12
17
|
# @param attributes [Hash] hash of element attributes
|
13
|
-
# @param
|
14
|
-
def initialize(tag, attributes
|
18
|
+
# @param element_styles [String] document styles tp apply to the element
|
19
|
+
def initialize(tag, attributes: {}, element_styles: '')
|
15
20
|
@tag = tag
|
16
|
-
element_styles = attributes.delete(:style)
|
17
21
|
@attrs = Attributes.new(attributes)
|
18
|
-
process_styles(
|
22
|
+
process_styles(element_styles, attributes['style'])
|
19
23
|
end
|
20
24
|
|
21
25
|
# Is a block tag?
|
@@ -73,23 +77,10 @@ module PrawnHtml
|
|
73
77
|
|
74
78
|
private
|
75
79
|
|
76
|
-
def
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
attrs['id'] ? "##{attrs['id']}" : nil
|
81
|
-
].compact!
|
82
|
-
document_styles.each_with_object({}) do |(sel, attributes), res|
|
83
|
-
res.merge!(attributes) if selectors.include?(sel)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def process_styles(document_styles, element_styles)
|
88
|
-
attrs.merge_styles!(attrs.process_styles(tag_styles)) if respond_to?(:tag_styles)
|
89
|
-
doc_styles = evaluate_document_styles(document_styles)
|
90
|
-
attrs.merge_styles!(doc_styles)
|
91
|
-
el_styles = Attributes.parse_styles(element_styles)
|
92
|
-
attrs.merge_styles!(attrs.process_styles(el_styles)) if el_styles
|
80
|
+
def process_styles(element_styles, inline_styles)
|
81
|
+
attrs.merge_text_styles!(tag_styles) if respond_to?(:tag_styles)
|
82
|
+
attrs.merge_text_styles!(element_styles)
|
83
|
+
attrs.merge_text_styles!(inline_styles)
|
93
84
|
end
|
94
85
|
end
|
95
86
|
end
|