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.
@@ -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