prawn-html 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,30 +4,38 @@ require 'oga'
4
4
 
5
5
  module PrawnHtml
6
6
  class HtmlParser
7
+ REGEXP_STYLES = /\s*([^{\s]+)\s*{\s*([^}]*?)\s*}/m.freeze
8
+
7
9
  # Init the HtmlParser
8
10
  #
9
- # @param pdf [Prawn::Document] Target Prawn PDF document
10
- def initialize(pdf)
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])
11
14
  @processing = false
12
- @renderer = DocumentRenderer.new(pdf)
15
+ @ignore = false
16
+ @ignore_content_tags = ignore_content_tags
17
+ @renderer = renderer
18
+ @styles = {}
13
19
  end
14
20
 
15
- # Processes HTML and renders it on the PDF document
21
+ # Processes HTML and renders it
16
22
  #
17
23
  # @param html [String] The HTML content to process
18
24
  def process(html)
19
25
  @processing = !html.include?('<body')
20
- doc = Oga.parse_html(html)
21
- traverse_nodes(doc.children)
26
+ @document = Oga.parse_html(html)
27
+ traverse_nodes(document.children)
22
28
  renderer.flush
23
29
  end
24
30
 
25
31
  private
26
32
 
27
- attr_reader :processing, :renderer
33
+ attr_reader :document, :ignore, :processing, :renderer, :styles
28
34
 
29
35
  def traverse_nodes(nodes)
30
36
  nodes.each do |node|
37
+ next if node.is_a?(Oga::XML::Comment)
38
+
31
39
  element = node_open(node)
32
40
  traverse_nodes(node.children) if node.children.any?
33
41
  node_close(element) if element
@@ -37,21 +45,27 @@ module PrawnHtml
37
45
  def node_open(node)
38
46
  tag = node.is_a?(Oga::XML::Element) && init_element(node)
39
47
  return unless processing
48
+ return IgnoredTag.new(tag) if ignore
40
49
  return renderer.on_text_node(node.text) unless tag
41
50
 
42
- attributes = prepare_attributes(node)
43
- renderer.on_tag_open(tag, attributes)
51
+ renderer.on_tag_open(tag, attributes: prepare_attributes(node), element_styles: styles[node])
44
52
  end
45
53
 
46
54
  def init_element(node)
47
55
  node.name.downcase.to_sym.tap do |tag_name|
48
56
  @processing = true if tag_name == :body
49
- renderer.assign_document_styles(extract_styles(node.text)) if tag_name == :style
57
+ @ignore = true if @processing && @ignore_content_tags.include?(tag_name)
58
+ process_styles(node.text) if tag_name == :style
50
59
  end
51
60
  end
52
61
 
53
- def extract_styles(text)
54
- text.scan(/\s*([^{\s]+)\s*{\s*([^}]*?)\s*}/m).to_h
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
55
69
  end
56
70
 
57
71
  def prepare_attributes(node)
@@ -61,10 +75,21 @@ module PrawnHtml
61
75
  end
62
76
 
63
77
  def node_close(element)
64
- renderer.on_tag_close(element) if @processing
78
+ if processing
79
+ renderer.on_tag_close(element) unless ignore
80
+ @ignore = false if ignore && @ignore_content_tags.include?(element.tag)
81
+ end
65
82
  @processing = false if element.tag == :body
66
83
  end
67
84
  end
68
85
 
86
+ class IgnoredTag
87
+ attr_accessor :tag
88
+
89
+ def initialize(tag_name)
90
+ @tag = tag_name
91
+ end
92
+ end
93
+
69
94
  HtmlHandler = HtmlParser
70
95
  end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module PrawnHtml
6
+ class PdfWrapper
7
+ extend Forwardable
8
+
9
+ def_delegators :@pdf, :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
+ # Calculate the height of a buffer of items
28
+ #
29
+ # @param buffer [Array] Buffer of items
30
+ # @param options [Hash] Output options
31
+ #
32
+ # @return [Float] calculated height
33
+ def calc_buffer_height(buffer, options)
34
+ pdf.height_of_formatted(buffer, options)
35
+ end
36
+
37
+ # Calculate the width of a buffer of items
38
+ #
39
+ # @param buffer [Array] Buffer of items
40
+ #
41
+ # @return [Float] calculated width
42
+ def calc_buffer_width(buffer)
43
+ width = 0
44
+ buffer.each do |item|
45
+ font_family = item[:font] || pdf.font.name
46
+ pdf.font(font_family, size: item[:size] || pdf.font_size) do
47
+ width += pdf.width_of(item[:text], inline_format: true)
48
+ end
49
+ end
50
+ width
51
+ end
52
+
53
+ # Height of the page
54
+ #
55
+ # @return [Float] height
56
+ def page_height
57
+ pdf.bounds.height
58
+ end
59
+
60
+ # Width of the page
61
+ #
62
+ # @return [Float] width
63
+ def page_width
64
+ pdf.bounds.width
65
+ end
66
+
67
+ # Draw a rectangle
68
+ #
69
+ # @param x [Float] left position of the rectangle
70
+ # @param y [Float] top position of the rectangle
71
+ # @param width [Float] width of the rectangle
72
+ # @param height [Float] height of the rectangle
73
+ # @param color [String] fill color
74
+ def draw_rectangle(x:, y:, width:, height:, color:)
75
+ current_fill_color = pdf.fill_color
76
+ pdf.fill_color = color
77
+ pdf.fill_rectangle([y, x], width, height)
78
+ pdf.fill_color = current_fill_color
79
+ end
80
+
81
+ # Horizontal line
82
+ #
83
+ # @param color [String] line color
84
+ # @param dash [Integer|Array] integer or array of integer with dash options
85
+ def horizontal_rule(color:, dash:)
86
+ current_color = pdf.stroke_color
87
+ pdf.dash(dash) if dash
88
+ pdf.stroke_color = color if color
89
+ pdf.stroke_horizontal_rule
90
+ pdf.stroke_color = current_color if color
91
+ pdf.undash if dash
92
+ end
93
+
94
+ # Image
95
+ #
96
+ # @param src [String] image source path
97
+ # @param options [Hash] hash of options
98
+ def image(src, options = {})
99
+ return unless src
100
+
101
+ pdf.image(src, options)
102
+ end
103
+
104
+ # Output to the PDF document
105
+ #
106
+ # @param buffer [Array] array of text items
107
+ # @param options [Hash] hash of options
108
+ # @param bounding_box [Array] bounding box arguments, if bounded
109
+ def puts(buffer, options, bounding_box: nil, left_indent: 0)
110
+ return output_buffer(buffer, options, left_indent: left_indent) unless bounding_box
111
+
112
+ current_y = pdf.cursor
113
+ pdf.bounding_box(*bounding_box) do
114
+ output_buffer(buffer, options, left_indent: left_indent)
115
+ end
116
+ pdf.move_cursor_to(current_y)
117
+ end
118
+
119
+ # Underline
120
+ #
121
+ # @param x1 [Float] left position of the line
122
+ # @param x2 [Float] right position of the line
123
+ # @param y [Float] vertical position of the line
124
+ def underline(x1:, x2:, y:)
125
+ pdf.stroke do
126
+ pdf.line [x1, y], [x2, y]
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ attr_reader :pdf
133
+
134
+ def output_buffer(buffer, options, left_indent:)
135
+ formatted_text = proc { pdf.formatted_text(buffer, options) }
136
+ return formatted_text.call if left_indent == 0
137
+
138
+ pdf.indent(left_indent, 0, &formatted_text)
139
+ end
140
+ end
141
+ end
@@ -2,7 +2,11 @@
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 Ol P Small Span U Ul].freeze
5
+ CALLBACKS = {
6
+ 'Background' => Callbacks::Background,
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
 
7
11
  attr_accessor :parent
8
12
  attr_reader :attrs, :tag
@@ -11,12 +15,11 @@ module PrawnHtml
11
15
  #
12
16
  # @param tag [Symbol] tag name
13
17
  # @param attributes [Hash] hash of element attributes
14
- # @param document_styles [Hash] hash of document styles
15
- def initialize(tag, attributes = {}, document_styles = {})
18
+ # @param options [Hash] options (container width/height/etc.)
19
+ def initialize(tag, attributes: {}, options: {})
16
20
  @tag = tag
17
- element_styles = attributes.delete(:style)
21
+ @options = options
18
22
  @attrs = Attributes.new(attributes)
19
- process_styles(document_styles, element_styles)
20
23
  end
21
24
 
22
25
  # Is a block tag?
@@ -35,6 +38,15 @@ module PrawnHtml
35
38
  block_styles
36
39
  end
37
40
 
41
+ # Process tag styles
42
+ #
43
+ # @param element_styles [String] extra styles to apply to the element
44
+ def process_styles(element_styles: nil)
45
+ attrs.merge_text_styles!(tag_styles, options: options) if respond_to?(:tag_styles)
46
+ attrs.merge_text_styles!(element_styles, options: options) if element_styles
47
+ attrs.merge_text_styles!(attrs.style, options: options)
48
+ end
49
+
38
50
  # Styles to apply on tag closing
39
51
  #
40
52
  # @return [Hash] hash of styles to apply
@@ -74,23 +86,6 @@ module PrawnHtml
74
86
 
75
87
  private
76
88
 
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
89
+ attr_reader :options
95
90
  end
96
91
  end
@@ -6,7 +6,13 @@ module PrawnHtml
6
6
  ELEMENTS = [:a].freeze
7
7
 
8
8
  def tag_styles
9
- attrs.href ? { 'href' => attrs.href } : {}
9
+ return unless attrs.href
10
+
11
+ <<~STYLES
12
+ color: #00E;
13
+ href: #{attrs.href};
14
+ text-decoration: underline;
15
+ STYLES
10
16
  end
11
17
  end
12
18
  end
@@ -6,9 +6,7 @@ module PrawnHtml
6
6
  ELEMENTS = [:b, :strong].freeze
7
7
 
8
8
  def tag_styles
9
- {
10
- 'font-weight' => 'bold'
11
- }
9
+ 'font-weight: bold'
12
10
  end
13
11
  end
14
12
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ module Tags
5
+ class Blockquote < Tag
6
+ ELEMENTS = [:blockquote].freeze
7
+
8
+ MARGIN_BOTTOM = 12.7
9
+ MARGIN_LEFT = 40.4
10
+ MARGIN_TOP = 12.7
11
+
12
+ def block?
13
+ true
14
+ end
15
+
16
+ def tag_styles
17
+ <<~STYLES
18
+ margin-bottom: #{MARGIN_BOTTOM}px;
19
+ margin-left: #{MARGIN_LEFT}px;
20
+ margin-top: #{MARGIN_TOP}px;
21
+ STYLES
22
+ end
23
+ end
24
+ end
25
+ end
@@ -4,6 +4,10 @@ module PrawnHtml
4
4
  module Tags
5
5
  class Body < Tag
6
6
  ELEMENTS = [:body].freeze
7
+
8
+ def block?
9
+ true
10
+ end
7
11
  end
8
12
  end
9
13
  end
@@ -5,17 +5,16 @@ module PrawnHtml
5
5
  class Br < Tag
6
6
  ELEMENTS = [:br].freeze
7
7
 
8
- BR_SPACING = 12
8
+ BR_SPACING = Utils.convert_size('17')
9
9
 
10
10
  def block?
11
11
  true
12
12
  end
13
13
 
14
14
  def custom_render(pdf, context)
15
- return if context.last_text_node
15
+ return if context.last_text_node || context.previous_tag != :br
16
16
 
17
- @spacing ||= Utils.convert_size(BR_SPACING.to_s)
18
- pdf.move_down(@spacing)
17
+ pdf.advance_cursor(BR_SPACING)
19
18
  end
20
19
  end
21
20
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ module Tags
5
+ class Code < Tag
6
+ ELEMENTS = [:code].freeze
7
+
8
+ def tag_styles
9
+ 'font-family: Courier'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -6,9 +6,7 @@ module PrawnHtml
6
6
  ELEMENTS = [:del, :s].freeze
7
7
 
8
8
  def tag_styles
9
- {
10
- 'callback' => Callbacks::StrikeThrough
11
- }
9
+ 'text-decoration: line-through'
12
10
  end
13
11
  end
14
12
  end
@@ -6,30 +6,30 @@ module PrawnHtml
6
6
  ELEMENTS = [:h1, :h2, :h3, :h4, :h5, :h6].freeze
7
7
 
8
8
  MARGINS_TOP = {
9
- h1: 25.5,
9
+ h1: 25,
10
10
  h2: 20.5,
11
- h3: 19,
12
- h4: 20,
11
+ h3: 18,
12
+ h4: 21.2,
13
13
  h5: 21.2,
14
- h6: 23.5
14
+ h6: 22.8
15
15
  }.freeze
16
16
 
17
17
  MARGINS_BOTTOM = {
18
- h1: 18.2,
19
- h2: 17.5,
20
- h3: 17.5,
21
- h4: 22,
22
- h5: 22,
23
- h6: 26.5
18
+ h1: 15.8,
19
+ h2: 15.8,
20
+ h3: 15.8,
21
+ h4: 20,
22
+ h5: 21.4,
23
+ h6: 24.8
24
24
  }.freeze
25
25
 
26
26
  SIZES = {
27
- h1: 31,
28
- h2: 23.5,
29
- h3: 18.2,
30
- h4: 16,
27
+ h1: 31.5,
28
+ h2: 24,
29
+ h3: 18.7,
30
+ h4: 15.7,
31
31
  h5: 13,
32
- h6: 10.5
32
+ h6: 10.8
33
33
  }.freeze
34
34
 
35
35
  def block?
@@ -37,12 +37,12 @@ module PrawnHtml
37
37
  end
38
38
 
39
39
  def tag_styles
40
- @tag_styles ||= {
41
- 'font-size' => SIZES[tag].to_s,
42
- 'font-weight' => 'bold',
43
- 'margin-bottom' => MARGINS_BOTTOM[tag].to_s,
44
- 'margin-top' => MARGINS_TOP[tag].to_s
45
- }
40
+ <<~STYLES
41
+ font-size: #{SIZES[tag]}px;
42
+ font-weight: bold;
43
+ margin-bottom: #{MARGINS_BOTTOM[tag]}px;
44
+ margin-top: #{MARGINS_TOP[tag]}px;
45
+ STYLES
46
46
  end
47
47
  end
48
48
  end