prawn-html 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +8 -2
- data/lib/prawn-html.rb +9 -5
- data/lib/prawn_html/attributes.rb +45 -33
- data/lib/prawn_html/callbacks/background.rb +19 -0
- data/lib/prawn_html/callbacks/strike_through.rb +4 -4
- data/lib/prawn_html/context.rb +22 -11
- data/lib/prawn_html/document_renderer.rb +56 -45
- data/lib/prawn_html/html_parser.rb +38 -13
- data/lib/prawn_html/pdf_wrapper.rb +141 -0
- data/lib/prawn_html/tag.rb +18 -23
- data/lib/prawn_html/tags/a.rb +7 -1
- data/lib/prawn_html/tags/b.rb +1 -3
- data/lib/prawn_html/tags/blockquote.rb +25 -0
- data/lib/prawn_html/tags/body.rb +4 -0
- data/lib/prawn_html/tags/br.rb +3 -4
- 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 +21 -21
- data/lib/prawn_html/tags/hr.rb +6 -16
- data/lib/prawn_html/tags/i.rb +1 -3
- data/lib/prawn_html/tags/img.rb +6 -5
- data/lib/prawn_html/tags/li.rb +20 -7
- data/lib/prawn_html/tags/mark.rb +1 -3
- data/lib/prawn_html/tags/ol.rb +20 -5
- data/lib/prawn_html/tags/p.rb +6 -6
- 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 +23 -4
- data/lib/prawn_html/utils.rb +48 -11
- data/lib/prawn_html/version.rb +1 -1
- metadata +9 -3
- data/lib/prawn_html/callbacks/highlight.rb +0 -21
@@ -4,30 +4,38 @@ require 'oga'
|
|
4
4
|
|
5
5
|
module PrawnHtml
|
6
6
|
class HtmlParser
|
7
|
+
REGEXP_STYLES = /\s*([^{\s]+)\s*{\s*([^}]*?)\s*}/m.freeze
|
8
|
+
|
7
9
|
# Init the HtmlParser
|
8
10
|
#
|
9
|
-
# @param
|
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])
|
11
14
|
@processing = false
|
12
|
-
@
|
15
|
+
@ignore = false
|
16
|
+
@ignore_content_tags = ignore_content_tags
|
17
|
+
@renderer = renderer
|
18
|
+
@styles = {}
|
13
19
|
end
|
14
20
|
|
15
|
-
# Processes HTML and renders it
|
21
|
+
# Processes HTML and renders it
|
16
22
|
#
|
17
23
|
# @param html [String] The HTML content to process
|
18
24
|
def process(html)
|
19
25
|
@processing = !html.include?('<body')
|
20
|
-
|
21
|
-
traverse_nodes(
|
26
|
+
@document = Oga.parse_html(html)
|
27
|
+
traverse_nodes(document.children)
|
22
28
|
renderer.flush
|
23
29
|
end
|
24
30
|
|
25
31
|
private
|
26
32
|
|
27
|
-
attr_reader :processing, :renderer
|
33
|
+
attr_reader :document, :ignore, :processing, :renderer, :styles
|
28
34
|
|
29
35
|
def traverse_nodes(nodes)
|
30
36
|
nodes.each do |node|
|
37
|
+
next if node.is_a?(Oga::XML::Comment)
|
38
|
+
|
31
39
|
element = node_open(node)
|
32
40
|
traverse_nodes(node.children) if node.children.any?
|
33
41
|
node_close(element) if element
|
@@ -37,21 +45,27 @@ module PrawnHtml
|
|
37
45
|
def node_open(node)
|
38
46
|
tag = node.is_a?(Oga::XML::Element) && init_element(node)
|
39
47
|
return unless processing
|
48
|
+
return IgnoredTag.new(tag) if ignore
|
40
49
|
return renderer.on_text_node(node.text) unless tag
|
41
50
|
|
42
|
-
attributes
|
43
|
-
renderer.on_tag_open(tag, attributes)
|
51
|
+
renderer.on_tag_open(tag, attributes: prepare_attributes(node), element_styles: styles[node])
|
44
52
|
end
|
45
53
|
|
46
54
|
def init_element(node)
|
47
55
|
node.name.downcase.to_sym.tap do |tag_name|
|
48
56
|
@processing = true if tag_name == :body
|
49
|
-
|
57
|
+
@ignore = true if @processing && @ignore_content_tags.include?(tag_name)
|
58
|
+
process_styles(node.text) if tag_name == :style
|
50
59
|
end
|
51
60
|
end
|
52
61
|
|
53
|
-
def
|
54
|
-
|
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
|
55
69
|
end
|
56
70
|
|
57
71
|
def prepare_attributes(node)
|
@@ -61,10 +75,21 @@ module PrawnHtml
|
|
61
75
|
end
|
62
76
|
|
63
77
|
def node_close(element)
|
64
|
-
|
78
|
+
if processing
|
79
|
+
renderer.on_tag_close(element) unless ignore
|
80
|
+
@ignore = false if ignore && @ignore_content_tags.include?(element.tag)
|
81
|
+
end
|
65
82
|
@processing = false if element.tag == :body
|
66
83
|
end
|
67
84
|
end
|
68
85
|
|
86
|
+
class IgnoredTag
|
87
|
+
attr_accessor :tag
|
88
|
+
|
89
|
+
def initialize(tag_name)
|
90
|
+
@tag = tag_name
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
69
94
|
HtmlHandler = HtmlParser
|
70
95
|
end
|
@@ -0,0 +1,141 @@
|
|
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
|
+
private
|
131
|
+
|
132
|
+
attr_reader :pdf
|
133
|
+
|
134
|
+
def output_buffer(buffer, options, left_indent:)
|
135
|
+
formatted_text = proc { pdf.formatted_text(buffer, options) }
|
136
|
+
return formatted_text.call if left_indent == 0
|
137
|
+
|
138
|
+
pdf.indent(left_indent, 0, &formatted_text)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/prawn_html/tag.rb
CHANGED
@@ -2,7 +2,11 @@
|
|
2
2
|
|
3
3
|
module PrawnHtml
|
4
4
|
class Tag
|
5
|
-
|
5
|
+
CALLBACKS = {
|
6
|
+
'Background' => Callbacks::Background,
|
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
|
|
7
11
|
attr_accessor :parent
|
8
12
|
attr_reader :attrs, :tag
|
@@ -11,12 +15,11 @@ module PrawnHtml
|
|
11
15
|
#
|
12
16
|
# @param tag [Symbol] tag name
|
13
17
|
# @param attributes [Hash] hash of element attributes
|
14
|
-
# @param
|
15
|
-
def initialize(tag, attributes
|
18
|
+
# @param options [Hash] options (container width/height/etc.)
|
19
|
+
def initialize(tag, attributes: {}, options: {})
|
16
20
|
@tag = tag
|
17
|
-
|
21
|
+
@options = options
|
18
22
|
@attrs = Attributes.new(attributes)
|
19
|
-
process_styles(document_styles, element_styles)
|
20
23
|
end
|
21
24
|
|
22
25
|
# Is a block tag?
|
@@ -35,6 +38,15 @@ module PrawnHtml
|
|
35
38
|
block_styles
|
36
39
|
end
|
37
40
|
|
41
|
+
# Process tag styles
|
42
|
+
#
|
43
|
+
# @param element_styles [String] extra styles to apply to the element
|
44
|
+
def process_styles(element_styles: nil)
|
45
|
+
attrs.merge_text_styles!(tag_styles, options: options) if respond_to?(:tag_styles)
|
46
|
+
attrs.merge_text_styles!(element_styles, options: options) if element_styles
|
47
|
+
attrs.merge_text_styles!(attrs.style, options: options)
|
48
|
+
end
|
49
|
+
|
38
50
|
# Styles to apply on tag closing
|
39
51
|
#
|
40
52
|
# @return [Hash] hash of styles to apply
|
@@ -74,23 +86,6 @@ module PrawnHtml
|
|
74
86
|
|
75
87
|
private
|
76
88
|
|
77
|
-
|
78
|
-
selectors = [
|
79
|
-
tag.to_s,
|
80
|
-
attrs['class'] ? ".#{attrs['class']}" : nil,
|
81
|
-
attrs['id'] ? "##{attrs['id']}" : nil
|
82
|
-
].compact!
|
83
|
-
document_styles.each_with_object({}) do |(sel, attributes), res|
|
84
|
-
res.merge!(attributes) if selectors.include?(sel)
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def process_styles(document_styles, element_styles)
|
89
|
-
attrs.merge_styles!(attrs.process_styles(tag_styles)) if respond_to?(:tag_styles)
|
90
|
-
doc_styles = evaluate_document_styles(document_styles)
|
91
|
-
attrs.merge_styles!(doc_styles)
|
92
|
-
el_styles = Attributes.parse_styles(element_styles)
|
93
|
-
attrs.merge_styles!(attrs.process_styles(el_styles)) if el_styles
|
94
|
-
end
|
89
|
+
attr_reader :options
|
95
90
|
end
|
96
91
|
end
|
data/lib/prawn_html/tags/a.rb
CHANGED
data/lib/prawn_html/tags/b.rb
CHANGED
@@ -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
|
data/lib/prawn_html/tags/body.rb
CHANGED
data/lib/prawn_html/tags/br.rb
CHANGED
@@ -5,17 +5,16 @@ module PrawnHtml
|
|
5
5
|
class Br < Tag
|
6
6
|
ELEMENTS = [:br].freeze
|
7
7
|
|
8
|
-
BR_SPACING =
|
8
|
+
BR_SPACING = Utils.convert_size('17')
|
9
9
|
|
10
10
|
def block?
|
11
11
|
true
|
12
12
|
end
|
13
13
|
|
14
14
|
def custom_render(pdf, context)
|
15
|
-
return if context.last_text_node
|
15
|
+
return if context.last_text_node || context.previous_tag != :br
|
16
16
|
|
17
|
-
|
18
|
-
pdf.move_down(@spacing)
|
17
|
+
pdf.advance_cursor(BR_SPACING)
|
19
18
|
end
|
20
19
|
end
|
21
20
|
end
|
data/lib/prawn_html/tags/del.rb
CHANGED
data/lib/prawn_html/tags/h.rb
CHANGED
@@ -6,30 +6,30 @@ module PrawnHtml
|
|
6
6
|
ELEMENTS = [:h1, :h2, :h3, :h4, :h5, :h6].freeze
|
7
7
|
|
8
8
|
MARGINS_TOP = {
|
9
|
-
h1: 25
|
9
|
+
h1: 25,
|
10
10
|
h2: 20.5,
|
11
|
-
h3:
|
12
|
-
h4:
|
11
|
+
h3: 18,
|
12
|
+
h4: 21.2,
|
13
13
|
h5: 21.2,
|
14
|
-
h6:
|
14
|
+
h6: 22.8
|
15
15
|
}.freeze
|
16
16
|
|
17
17
|
MARGINS_BOTTOM = {
|
18
|
-
h1:
|
19
|
-
h2:
|
20
|
-
h3:
|
21
|
-
h4:
|
22
|
-
h5:
|
23
|
-
h6:
|
18
|
+
h1: 15.8,
|
19
|
+
h2: 15.8,
|
20
|
+
h3: 15.8,
|
21
|
+
h4: 20,
|
22
|
+
h5: 21.4,
|
23
|
+
h6: 24.8
|
24
24
|
}.freeze
|
25
25
|
|
26
26
|
SIZES = {
|
27
|
-
h1: 31,
|
28
|
-
h2:
|
29
|
-
h3: 18.
|
30
|
-
h4:
|
27
|
+
h1: 31.5,
|
28
|
+
h2: 24,
|
29
|
+
h3: 18.7,
|
30
|
+
h4: 15.7,
|
31
31
|
h5: 13,
|
32
|
-
h6: 10.
|
32
|
+
h6: 10.8
|
33
33
|
}.freeze
|
34
34
|
|
35
35
|
def block?
|
@@ -37,12 +37,12 @@ module PrawnHtml
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def tag_styles
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
40
|
+
<<~STYLES
|
41
|
+
font-size: #{SIZES[tag]}px;
|
42
|
+
font-weight: bold;
|
43
|
+
margin-bottom: #{MARGINS_BOTTOM[tag]}px;
|
44
|
+
margin-top: #{MARGINS_TOP[tag]}px;
|
45
|
+
STYLES
|
46
46
|
end
|
47
47
|
end
|
48
48
|
end
|