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.
@@ -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
@@ -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.options[:before_content].to_s
37
+ last.tag_styles[:before_content].to_s
19
38
  end
20
39
 
21
- # Merges the context options
40
+ # Merges the context block styles
22
41
  #
23
- # @return [Hash] the hash of merged options
24
- def merge_options
42
+ # @return [Hash] the hash of merged styles
43
+ def block_styles
25
44
  each_with_object({}) do |element, res|
26
- element.options.each do |key, value|
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 merge_styles
36
- context_styles = each_with_object({}) do |element, res|
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.each do |key, val|
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 [Prawn::Document] target Prawn PDF document
10
+ # @param pdf [PdfWrapper] target PDF wrapper
12
11
  def initialize(pdf)
13
12
  @buffer = []
14
13
  @context = Context.new
15
- @doc_styles = {}
14
+ @document_styles = {}
16
15
  @pdf = pdf
17
16
  end
18
17
 
19
- # Assigns the document styles
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
- @doc_styles = styles.transform_values do |style_rules|
24
- Attributes.new(style: style_rules).styles
25
- end
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 [Tags::Base] closing element wrapper
31
+ # @param element [Tag] closing element wrapper
31
32
  def on_tag_close(element)
32
33
  render_if_needed(element)
33
- apply_post_styles(element&.post_styles)
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 tag [String] the tag name of the opening element
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 [Tags::Base] the opening element wrapper
44
- def on_tag_open(tag, attributes)
45
- tag_class = tag_classes[tag]
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(tag, attributes).tap do |element|
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 = content.gsub(/\A\s*\n\s*|\s*\n\s*\Z/, '').delete("\n").squeeze(' ')
62
- buffer << context.merge_styles.merge(text: ::Oga::HTML::Entities.decode(context.before_content) + text)
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
- options = context.merge_options.slice(:align, :leading, :margin_left, :padding_left)
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, :doc_styles, :pdf
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
- apply_pre_styles(element)
92
- element.apply_doc_styles(doc_styles)
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 apply_post_styles(styles)
110
- context.last_margin = styles[:margin_bottom].to_f
111
- return if !styles || styles.empty?
112
-
113
- pdf.move_down(context.last_margin.round(4)) if context.last_margin > 0
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 apply_pre_styles(element)
118
- pdf.move_down(element.options[:padding_top].round(4)) if element.options.include?(:padding_top)
119
- return if !element.pre_styles || element.pre_styles.empty?
120
-
121
- margin = (element.pre_styles[:margin_top] - context.last_margin).round(4)
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, options)
117
+ def output_content(buffer, block_styles)
126
118
  buffer.each { |item| item[:callback] = item[:callback].new(pdf, item) if item[:callback] }
127
- if (left = options.delete(:margin_left).to_f + options.delete(:padding_left).to_f) > 0
128
- pdf.indent(left) { pdf.formatted_text(buffer, options) }
129
- else
130
- pdf.formatted_text(buffer, options)
131
- end
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 HtmlHandler
7
- # Init the HtmlHandler
6
+ class HtmlParser
7
+ # Init the HtmlParser
8
8
  #
9
- # @param pdf [Prawn::Document] Target Prawn PDF document
10
- def initialize(pdf)
9
+ # @param renderer [DocumentRenderer] document renderer
10
+ def initialize(renderer)
11
11
  @processing = false
12
- @renderer = DocumentRenderer.new(pdf)
12
+ @renderer = renderer
13
13
  end
14
14
 
15
- # Processes HTML and renders it on the PDF document
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 && !@processing
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