prawn-html 0.3.2 → 0.6.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,12 +4,18 @@ 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
11
  # @param renderer [DocumentRenderer] document renderer
10
- def initialize(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
15
+ @ignore = false
16
+ @ignore_content_tags = ignore_content_tags
12
17
  @renderer = renderer
18
+ @styles = {}
13
19
  end
14
20
 
15
21
  # Processes HTML and renders it
@@ -17,17 +23,19 @@ module PrawnHtml
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
@@ -6,7 +6,7 @@ module PrawnHtml
6
6
  class PdfWrapper
7
7
  extend Forwardable
8
8
 
9
- def_delegators :@pdf, :bounds, :start_new_page
9
+ def_delegators :@pdf, :start_new_page
10
10
 
11
11
  # Wrapper for Prawn PDF Document
12
12
  #
@@ -24,6 +24,46 @@ module PrawnHtml
24
24
  pdf.move_down(move_down)
25
25
  end
26
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
+
27
67
  # Draw a rectangle
28
68
  #
29
69
  # @param x [Float] left position of the rectangle
@@ -66,12 +106,12 @@ module PrawnHtml
66
106
  # @param buffer [Array] array of text items
67
107
  # @param options [Hash] hash of options
68
108
  # @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
109
+ def puts(buffer, options, bounding_box: nil, left_indent: 0)
110
+ return output_buffer(buffer, options, left_indent: left_indent) unless bounding_box
71
111
 
72
112
  current_y = pdf.cursor
73
113
  pdf.bounding_box(*bounding_box) do
74
- pdf.formatted_text(buffer, options)
114
+ output_buffer(buffer, options, left_indent: left_indent)
75
115
  end
76
116
  pdf.move_cursor_to(current_y)
77
117
  end
@@ -90,5 +130,12 @@ module PrawnHtml
90
130
  private
91
131
 
92
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
93
140
  end
94
141
  end
@@ -2,7 +2,16 @@
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
+ extend Forwardable
6
+
7
+ CALLBACKS = {
8
+ 'Background' => Callbacks::Background,
9
+ 'StrikeThrough' => Callbacks::StrikeThrough
10
+ }.freeze
11
+
12
+ 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
13
+
14
+ def_delegators :@attrs, :styles, :update_styles
6
15
 
7
16
  attr_accessor :parent
8
17
  attr_reader :attrs, :tag
@@ -11,12 +20,11 @@ module PrawnHtml
11
20
  #
12
21
  # @param tag [Symbol] tag name
13
22
  # @param attributes [Hash] hash of element attributes
14
- # @param document_styles [Hash] hash of document styles
15
- def initialize(tag, attributes = {}, document_styles = {})
23
+ # @param options [Hash] options (container width/height/etc.)
24
+ def initialize(tag, attributes: {}, options: {})
16
25
  @tag = tag
17
- element_styles = attributes.delete(:style)
26
+ @options = options
18
27
  @attrs = Attributes.new(attributes)
19
- process_styles(document_styles, element_styles)
20
28
  end
21
29
 
22
30
  # Is a block tag?
@@ -35,6 +43,16 @@ module PrawnHtml
35
43
  block_styles
36
44
  end
37
45
 
46
+ # Process tag styles
47
+ #
48
+ # @param element_styles [String] extra styles to apply to the element
49
+ def process_styles(element_styles: nil)
50
+ attrs.merge_text_styles!(tag_styles, options: options) if respond_to?(:tag_styles)
51
+ attrs.merge_text_styles!(element_styles, options: options) if element_styles
52
+ attrs.merge_text_styles!(attrs.style, options: options)
53
+ attrs.merge_text_styles!(extra_styles, options: options) if respond_to?(:extra_styles)
54
+ end
55
+
38
56
  # Styles to apply on tag closing
39
57
  #
40
58
  # @return [Hash] hash of styles to apply
@@ -42,13 +60,6 @@ module PrawnHtml
42
60
  styles.slice(*Attributes::STYLES_APPLY[:tag_close])
43
61
  end
44
62
 
45
- # Styles hash
46
- #
47
- # @return [Hash] hash of styles
48
- def styles
49
- attrs.styles
50
- end
51
-
52
63
  # Styles to apply on tag opening
53
64
  #
54
65
  # @return [Hash] hash of styles to apply
@@ -74,23 +85,6 @@ module PrawnHtml
74
85
 
75
86
  private
76
87
 
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
88
+ attr_reader :options
95
89
  end
96
90
  end
@@ -5,8 +5,15 @@ module PrawnHtml
5
5
  class A < Tag
6
6
  ELEMENTS = [:a].freeze
7
7
 
8
+ def extra_styles
9
+ attrs.href ? "href: #{attrs.href}" : nil
10
+ end
11
+
8
12
  def tag_styles
9
- attrs.href ? { 'href' => attrs.href } : {}
13
+ <<~STYLES
14
+ color: #00E;
15
+ text-decoration: underline;
16
+ STYLES
10
17
  end
11
18
  end
12
19
  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,14 +5,14 @@ module PrawnHtml
5
5
  class Br < Tag
6
6
  ELEMENTS = [:br].freeze
7
7
 
8
- BR_SPACING = Utils.convert_size('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
17
  pdf.advance_cursor(BR_SPACING)
18
18
  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