prawn-html 0.3.0 → 0.5.0

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