prawn-html 0.1.0 → 0.3.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 +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
|