prawn-html 0.2.0 → 0.4.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.
@@ -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