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