prawn-html 0.1.4 → 0.4.0
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 +51 -8
- data/lib/prawn-html.rb +170 -14
- data/lib/prawn_html/attributes.rb +71 -110
- 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 +29 -12
- data/lib/prawn_html/document_renderer.rb +45 -50
- data/lib/prawn_html/html_parser.rb +95 -0
- data/lib/prawn_html/pdf_wrapper.rb +94 -0
- data/lib/prawn_html/tag.rb +86 -0
- data/lib/prawn_html/tags/a.rb +3 -5
- data/lib/prawn_html/tags/b.rb +3 -7
- data/lib/prawn_html/tags/blockquote.rb +25 -0
- data/lib/prawn_html/tags/body.rb +1 -3
- data/lib/prawn_html/tags/br.rb +3 -6
- data/lib/prawn_html/tags/del.rb +3 -7
- data/lib/prawn_html/tags/div.rb +1 -3
- data/lib/prawn_html/tags/h.rb +8 -10
- data/lib/prawn_html/tags/hr.rb +19 -9
- data/lib/prawn_html/tags/i.rb +3 -7
- data/lib/prawn_html/tags/img.rb +11 -13
- data/lib/prawn_html/tags/li.rb +8 -7
- data/lib/prawn_html/tags/mark.rb +3 -7
- data/lib/prawn_html/tags/ol.rb +26 -0
- data/lib/prawn_html/tags/p.rb +6 -8
- data/lib/prawn_html/tags/small.rb +1 -3
- data/lib/prawn_html/tags/span.rb +1 -3
- data/lib/prawn_html/tags/u.rb +3 -5
- data/lib/prawn_html/tags/ul.rb +3 -7
- data/lib/prawn_html/utils.rb +107 -0
- data/lib/prawn_html/version.rb +1 -1
- metadata +8 -4
- data/lib/prawn_html/html_handler.rb +0 -68
- data/lib/prawn_html/tags/base.rb +0 -54
@@ -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
@@ -10,34 +10,50 @@ module PrawnHtml
|
|
10
10
|
def initialize(*_args)
|
11
11
|
super
|
12
12
|
@last_margin = 0
|
13
|
+
@last_text_node = false
|
13
14
|
end
|
14
15
|
|
15
|
-
|
16
|
-
|
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
|
17
30
|
|
18
|
-
|
31
|
+
# Evaluate before content
|
32
|
+
#
|
33
|
+
# @return [String] before content string
|
34
|
+
def before_content
|
35
|
+
(last.respond_to?(:before_content) && last.before_content) || ''
|
19
36
|
end
|
20
37
|
|
21
|
-
# Merges the context
|
38
|
+
# Merges the context block styles
|
22
39
|
#
|
23
|
-
# @return [Hash] the hash of merged
|
24
|
-
def
|
40
|
+
# @return [Hash] the hash of merged styles
|
41
|
+
def block_styles
|
25
42
|
each_with_object({}) do |element, res|
|
26
|
-
element.
|
43
|
+
element.block_styles.each do |key, value|
|
27
44
|
Attributes.merge_attr!(res, key, value)
|
28
45
|
end
|
29
46
|
end
|
30
47
|
end
|
31
48
|
|
32
|
-
# Merge the context styles
|
49
|
+
# Merge the context styles for text nodes
|
33
50
|
#
|
34
51
|
# @return [Hash] the hash of merged styles
|
35
|
-
def
|
36
|
-
|
52
|
+
def text_node_styles
|
53
|
+
each_with_object(base_styles) do |element, res|
|
37
54
|
evaluate_element_styles(element, res)
|
38
55
|
element.update_styles(res) if element.respond_to?(:update_styles)
|
39
56
|
end
|
40
|
-
base_styles.merge(context_styles)
|
41
57
|
end
|
42
58
|
|
43
59
|
private
|
@@ -49,7 +65,8 @@ module PrawnHtml
|
|
49
65
|
end
|
50
66
|
|
51
67
|
def evaluate_element_styles(element, res)
|
52
|
-
element.styles.
|
68
|
+
styles = element.styles.slice(*Attributes::STYLES_APPLY[:text_node])
|
69
|
+
styles.each do |key, val|
|
53
70
|
if res.include?(key) && res[key].is_a?(Array)
|
54
71
|
res[key] += val
|
55
72
|
else
|
@@ -4,48 +4,38 @@ module PrawnHtml
|
|
4
4
|
class DocumentRenderer
|
5
5
|
NEW_LINE = { text: "\n" }.freeze
|
6
6
|
SPACE = { text: ' ' }.freeze
|
7
|
-
TAG_CLASSES = [Tags::A, Tags::B, Tags::Body, Tags::Br, Tags::Del, Tags::Div, Tags::H, Tags::Hr, Tags::I, Tags::Img, Tags::Li, Tags::Mark, Tags::P, Tags::Small, Tags::Span, Tags::U, Tags::Ul].freeze
|
8
7
|
|
9
8
|
# Init the DocumentRenderer
|
10
9
|
#
|
11
|
-
# @param pdf [
|
10
|
+
# @param pdf [PdfWrapper] target PDF wrapper
|
12
11
|
def initialize(pdf)
|
13
12
|
@buffer = []
|
14
13
|
@context = Context.new
|
15
|
-
@doc_styles = {}
|
16
14
|
@pdf = pdf
|
17
15
|
end
|
18
16
|
|
19
|
-
# Assigns the document styles
|
20
|
-
#
|
21
|
-
# @param styles [Hash] styles hash with CSS selectors as keys and rules as values
|
22
|
-
def assign_document_styles(styles)
|
23
|
-
@doc_styles = styles.transform_values do |style_rules|
|
24
|
-
Attributes.new(style: style_rules).styles
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
17
|
# On tag close callback
|
29
18
|
#
|
30
|
-
# @param element [
|
19
|
+
# @param element [Tag] closing element wrapper
|
31
20
|
def on_tag_close(element)
|
32
21
|
render_if_needed(element)
|
33
|
-
|
22
|
+
apply_tag_close_styles(element)
|
34
23
|
context.last_text_node = false
|
35
24
|
context.pop
|
36
25
|
end
|
37
26
|
|
38
27
|
# On tag open callback
|
39
28
|
#
|
40
|
-
# @param
|
29
|
+
# @param tag_name [String] the tag name of the opening element
|
41
30
|
# @param attributes [Hash] an hash of the element attributes
|
31
|
+
# @param element_styles [String] document styles to apply to the element
|
42
32
|
#
|
43
|
-
# @return [
|
44
|
-
def on_tag_open(
|
45
|
-
tag_class =
|
33
|
+
# @return [Tag] the opening element wrapper
|
34
|
+
def on_tag_open(tag_name, attributes:, element_styles: '')
|
35
|
+
tag_class = Tag.class_for(tag_name)
|
46
36
|
return unless tag_class
|
47
37
|
|
48
|
-
tag_class.new(
|
38
|
+
tag_class.new(tag_name, attributes: attributes, element_styles: element_styles).tap do |element|
|
49
39
|
setup_element(element)
|
50
40
|
end
|
51
41
|
end
|
@@ -58,8 +48,9 @@ module PrawnHtml
|
|
58
48
|
def on_text_node(content)
|
59
49
|
return if content.match?(/\A\s*\Z/)
|
60
50
|
|
61
|
-
text =
|
62
|
-
|
51
|
+
text = ::Oga::HTML::Entities.decode(context.before_content)
|
52
|
+
text += content.gsub(/\A\s*\n\s*|\s*\n\s*\Z/, '').delete("\n").squeeze(' ')
|
53
|
+
buffer << context.text_node_styles.merge(text: text)
|
63
54
|
context.last_text_node = true
|
64
55
|
nil
|
65
56
|
end
|
@@ -68,8 +59,7 @@ module PrawnHtml
|
|
68
59
|
def render
|
69
60
|
return if buffer.empty?
|
70
61
|
|
71
|
-
|
72
|
-
output_content(buffer.dup, options)
|
62
|
+
output_content(buffer.dup, context.block_styles)
|
73
63
|
buffer.clear
|
74
64
|
context.last_margin = 0
|
75
65
|
end
|
@@ -78,19 +68,12 @@ module PrawnHtml
|
|
78
68
|
|
79
69
|
private
|
80
70
|
|
81
|
-
attr_reader :buffer, :context, :
|
82
|
-
|
83
|
-
def tag_classes
|
84
|
-
@tag_classes ||= TAG_CLASSES.each_with_object({}) do |klass, res|
|
85
|
-
res.merge!(klass.elements)
|
86
|
-
end
|
87
|
-
end
|
71
|
+
attr_reader :buffer, :context, :pdf
|
88
72
|
|
89
73
|
def setup_element(element)
|
90
74
|
add_space_if_needed unless render_if_needed(element)
|
91
|
-
|
92
|
-
|
93
|
-
context.push(element)
|
75
|
+
apply_tag_open_styles(element)
|
76
|
+
context.add(element)
|
94
77
|
element.custom_render(pdf, context) if element.respond_to?(:custom_render)
|
95
78
|
end
|
96
79
|
|
@@ -106,29 +89,41 @@ module PrawnHtml
|
|
106
89
|
true
|
107
90
|
end
|
108
91
|
|
109
|
-
def
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
pdf.
|
114
|
-
pdf.move_down(styles[:padding_bottom].round(4)) if styles[:padding_bottom].to_f > 0
|
92
|
+
def apply_tag_close_styles(element)
|
93
|
+
tag_styles = element.tag_close_styles
|
94
|
+
context.last_margin = tag_styles[:margin_bottom].to_f
|
95
|
+
pdf.advance_cursor(context.last_margin + tag_styles[:padding_bottom].to_f)
|
96
|
+
pdf.start_new_page if tag_styles[:break_after]
|
115
97
|
end
|
116
98
|
|
117
|
-
def
|
118
|
-
|
119
|
-
|
99
|
+
def apply_tag_open_styles(element)
|
100
|
+
tag_styles = element.tag_open_styles
|
101
|
+
move_down = (tag_styles[:margin_top].to_f - context.last_margin) + tag_styles[:padding_top].to_f
|
102
|
+
pdf.advance_cursor(move_down) if move_down > 0
|
103
|
+
pdf.start_new_page if tag_styles[:break_before]
|
104
|
+
end
|
120
105
|
|
121
|
-
|
122
|
-
|
106
|
+
def output_content(buffer, block_styles)
|
107
|
+
apply_callbacks(buffer)
|
108
|
+
left_indent = block_styles[:margin_left].to_f + block_styles[:padding_left].to_f
|
109
|
+
options = block_styles.slice(:align, :leading, :mode, :padding_left)
|
110
|
+
options[:indent_paragraphs] = left_indent if left_indent > 0
|
111
|
+
pdf.puts(buffer, options, bounding_box: bounds(block_styles))
|
123
112
|
end
|
124
113
|
|
125
|
-
def
|
126
|
-
buffer.
|
127
|
-
|
128
|
-
|
129
|
-
else
|
130
|
-
pdf.formatted_text(buffer, options)
|
114
|
+
def apply_callbacks(buffer)
|
115
|
+
buffer.select { |item| item[:callback] }.each do |item|
|
116
|
+
callback = Tag::CALLBACKS[item[:callback]]
|
117
|
+
item[:callback] = callback.new(pdf, item)
|
131
118
|
end
|
132
119
|
end
|
120
|
+
|
121
|
+
def bounds(block_styles)
|
122
|
+
return unless block_styles[:position] == :absolute
|
123
|
+
|
124
|
+
y = pdf.bounds.height - (block_styles[:top] || 0)
|
125
|
+
w = pdf.bounds.width - (block_styles[:left] || 0)
|
126
|
+
[[block_styles[:left] || 0, y], { width: w }]
|
127
|
+
end
|
133
128
|
end
|
134
129
|
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
|