prawn-html 0.2.0 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,10 +11,8 @@ module PrawnHtml
11
11
  end
12
12
 
13
13
  def render_behind(fragment)
14
- original_color = @pdf.fill_color
15
- @pdf.fill_color = @color
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
- y = (fragment.top_left[1] + fragment.bottom_left[1]) / 2
12
- @pdf.stroke do
13
- @pdf.line [fragment.top_left[0], y], [fragment.top_right[0], y]
14
- end
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
@@ -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
- return '' if empty? || !last.respond_to?(:tag_styles)
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 [Prawn::Document] target Prawn PDF document
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, document_styles).tap do |element|
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
- text = ::Oga::HTML::Entities.decode(context.before_content)
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
- options = context.block_styles.slice(:align, :leading, :margin_left, :mode, :padding_left)
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, :document_styles, :pdf
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.push(element)
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
- move_down = context.last_margin + tag_styles[:padding_bottom].to_f
106
- pdf.move_down(move_down) if move_down > 0
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.move_down(move_down) if move_down > 0
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, options)
116
- buffer.each { |item| item[:callback] = item[:callback].new(pdf, item) if item[:callback] }
117
- left_indent = options.delete(:margin_left).to_f + options.delete(:padding_left).to_f
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.formatted_text(buffer, options)
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
@@ -2,20 +2,24 @@
2
2
 
3
3
  module PrawnHtml
4
4
  class Tag
5
- TAG_CLASSES = %w[A B Body Br Del Div H Hr I Img Li Mark P Small Span U Ul].freeze
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 document_styles [Hash] hash of document styles
14
- def initialize(tag, attributes = {}, document_styles = {})
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(document_styles, element_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 evaluate_document_styles(document_styles)
77
- selectors = [
78
- tag.to_s,
79
- attrs['class'] ? ".#{attrs['class']}" : nil,
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
@@ -6,7 +6,7 @@ module PrawnHtml
6
6
  ELEMENTS = [:a].freeze
7
7
 
8
8
  def tag_styles
9
- attrs.href ? { 'href' => attrs.href } : {}
9
+ "href: #{attrs.href}" if attrs.href
10
10
  end
11
11
  end
12
12
  end