prawn-html 0.2.0 → 0.4.2
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 +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
|