prawn-html 0.4.0 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4dec0bd9a3746705bdaca34d49c8a8eff5700fd8a5feebc3b3452001cd239b0b
4
- data.tar.gz: 74b5e31d3056560435ea939f50f713f11c44ad4c1f584873203b910754e1f782
3
+ metadata.gz: 105884df1bf42d6aa44ff0a102a025960740c0d5a771c27e75949113cd7b9b70
4
+ data.tar.gz: abe3998f3f65cd2b421280d99151f18e321ac650dd8abed26260062e6f8e455a
5
5
  SHA512:
6
- metadata.gz: 814c481a5fce1da43dcb04254ed97f76f234fbdcaa6826070b00594f80cd3b4396c190a2972c0f6f72e00d35e5fd1b534cf22c9ef932b8ee308562de7c6bc854
7
- data.tar.gz: f99ce1543d1b28689b19ceddd8e86a73fae8e204bc98270e6a422dd93e44d89ff50366969b67e0fb2a9a4e91d03367981ab5c0f9481b91378df57e2304358f40
6
+ metadata.gz: 0dc20b1ee2c6bd1321b4359e31e217cea923c68f5f3af242b097d56d9492f198e1df6af263cadcf75978c83700a90cad3cfbae08d25e9b0ebb39ddea6ad2c059
7
+ data.tar.gz: 88180bff0e1aac88d566911fafac9c6b6ae799eee6c1f77948f0bc811825f03c2e7ca2c8b1bf8962dc60e27ba355904d77289e3870e1c3faecd8c362e6536237
data/README.md CHANGED
@@ -35,30 +35,34 @@ To check some examples with the PDF output see [examples](examples/) folder.
35
35
 
36
36
  ## Supported tags & attributes
37
37
 
38
- HTML tags:
39
-
40
- - **a**: link
41
- - **b**: bold
42
- - **blockquote**: block quotation element
43
- - **br**: new line
44
- - **del**: strike-through
45
- - **div**: block element
46
- - **em**: italic
47
- - **h1** - **h6**: headings
48
- - **hr**: horizontal line
49
- - **i**: italic
50
- - **ins**: underline
51
- - **img**: image
52
- - **li**: list item
53
- - **mark**: highlight
54
- - **ol**: ordered list
55
- - **p**: block element
56
- - **s**: strike-through
57
- - **small**: smaller text
58
- - **span**: inline element
59
- - **strong**: bold
60
- - **u**: underline
61
- - **ul**: unordered list
38
+ HTML tags (using MDN definitions):
39
+
40
+ - **a**: the Anchor element
41
+ - **b**: the Bring Attention To element
42
+ - **blockquote**: the Block Quotation element
43
+ - **br**: the Line Break element
44
+ - **code**: the Inline Code element
45
+ - **del**: the Deleted Text element
46
+ - **div**: the Content Division element
47
+ - **em**: the Emphasis element
48
+ - **h1** - **h6**: the HTML Section Heading elements
49
+ - **hr**: the Thematic Break (Horizontal Rule) element
50
+ - **i**: the Idiomatic Text element
51
+ - **ins**: the added text element
52
+ - **img**: the Image Embed element
53
+ - **li**: the list item element
54
+ - **mark**: the Mark Text element
55
+ - **ol**: the Ordered List element
56
+ - **p**: the Paragraph element
57
+ - **pre**: the Preformatted Text element
58
+ - **s**: the strike-through text element
59
+ - **small**: the side comment element
60
+ - **span**: the generic inline element
61
+ - **strong**: the Strong Importance element
62
+ - **sub**: the Subscript element
63
+ - **sup**: the Superscript element
64
+ - **u**: the Unarticulated Annotation (Underline) element
65
+ - **ul**: the Unordered List element
62
66
 
63
67
  CSS attributes (dimensional units are ignored and considered in pixel):
64
68
 
@@ -86,6 +90,8 @@ CSS attributes (dimensional units are ignored and considered in pixel):
86
90
  - **top**: see *position (absolute)*
87
91
  - **width**: for *img* tag, support also percentage, ex. `<img src="image.jpg" style="width: 50%; height: 200px"/>`
88
92
 
93
+ The above attributes supports the `initial` value to reset them to their original value.
94
+
89
95
  For colors, the supported formats are:
90
96
  - 3 hex digits, ex. `color: #FB1`;
91
97
  - 6 hex digits, ex. `color: #abcdef`;
data/lib/prawn-html.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PrawnHtml
4
- PX = 0.66 # conversion constant for pixel sixes
4
+ ADJUST_LEADING = { nil => 0.18, 'Courier' => -0.07, 'Helvetica' => -0.17, 'Times-Roman' => 0.03 }.freeze
5
+ PX = 0.6 # conversion constant for pixel sixes
5
6
 
6
7
  COLORS = {
7
8
  'aliceblue' => 'f0f8ff',
@@ -1,31 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'ostruct'
4
+ require 'set'
4
5
 
5
6
  module PrawnHtml
6
7
  class Attributes < OpenStruct
7
- attr_reader :styles
8
+ attr_reader :initial, :styles
8
9
 
9
10
  STYLES_APPLY = {
10
- block: %i[align leading left margin_left padding_left position top],
11
+ block: %i[align bottom leading left margin_left padding_left position right top],
11
12
  tag_close: %i[margin_bottom padding_bottom break_after],
12
13
  tag_open: %i[margin_top padding_top break_before],
13
- text_node: %i[background callback character_spacing color font link list_style_type size styles]
14
+ text_node: %i[callback character_spacing color font link list_style_type size styles white_space]
14
15
  }.freeze
15
16
 
16
17
  STYLES_LIST = {
17
18
  # text node styles
18
- 'background' => { key: :background, set: :convert_color },
19
- 'callback' => { key: :callback, set: :copy_value },
19
+ 'background' => { key: :callback, set: :callback_background },
20
20
  'color' => { key: :color, set: :convert_color },
21
21
  'font-family' => { key: :font, set: :unquote },
22
22
  'font-size' => { key: :size, set: :convert_size },
23
- 'font-style' => { key: :styles, set: :append_styles },
24
- 'font-weight' => { key: :styles, set: :append_styles },
23
+ 'font-style' => { key: :styles, set: :append_styles, values: %i[italic] },
24
+ 'font-weight' => { key: :styles, set: :append_styles, values: %i[bold] },
25
25
  'href' => { key: :link, set: :copy_value },
26
26
  'letter-spacing' => { key: :character_spacing, set: :convert_float },
27
27
  'list-style-type' => { key: :list_style_type, set: :unquote },
28
- 'text-decoration' => { key: :styles, set: :append_styles },
28
+ 'text-decoration' => { key: :styles, set: :append_styles, values: %i[underline] },
29
+ 'vertical-align' => { key: :styles, set: :append_styles, values: %i[subscript superscript] },
30
+ 'white-space' => { key: :white_space, set: :convert_symbol },
29
31
  # tag opening styles
30
32
  'break-before' => { key: :break_before, set: :convert_symbol },
31
33
  'margin-top' => { key: :margin_top, set: :convert_size },
@@ -35,13 +37,17 @@ module PrawnHtml
35
37
  'margin-bottom' => { key: :margin_bottom, set: :convert_size },
36
38
  'padding-bottom' => { key: :padding_bottom, set: :convert_size },
37
39
  # block styles
38
- 'left' => { key: :left, set: :convert_size },
40
+ 'bottom' => { key: :bottom, set: :convert_size, options: :height },
41
+ 'left' => { key: :left, set: :convert_size, options: :width },
39
42
  'line-height' => { key: :leading, set: :convert_size },
40
43
  'margin-left' => { key: :margin_left, set: :convert_size },
41
44
  'padding-left' => { key: :padding_left, set: :convert_size },
42
45
  'position' => { key: :position, set: :convert_symbol },
46
+ 'right' => { key: :right, set: :convert_size, options: :width },
43
47
  'text-align' => { key: :align, set: :convert_symbol },
44
- 'top' => { key: :top, set: :convert_size }
48
+ 'top' => { key: :top, set: :convert_size, options: :height },
49
+ # special styles
50
+ 'text-decoration-line-through' => { key: :callback, set: :callback_strike_through }
45
51
  }.freeze
46
52
 
47
53
  STYLES_MERGE = %i[margin_left padding_left].freeze
@@ -50,6 +56,7 @@ module PrawnHtml
50
56
  def initialize(attributes = {})
51
57
  super
52
58
  @styles = {} # result styles
59
+ @initial = Set.new
53
60
  end
54
61
 
55
62
  # Processes the data attributes
@@ -65,9 +72,37 @@ module PrawnHtml
65
72
  # Merge text styles
66
73
  #
67
74
  # @param text_styles [String] styles to parse and process
68
- def merge_text_styles!(text_styles)
75
+ # @param options [Hash] options (container width/height/etc.)
76
+ def merge_text_styles!(text_styles, options: {})
69
77
  hash_styles = Attributes.parse_styles(text_styles)
70
- process_styles(hash_styles) unless hash_styles.empty?
78
+ process_styles(hash_styles, options: options) unless hash_styles.empty?
79
+ end
80
+
81
+ # Remove an attribute value from the context styles
82
+ #
83
+ # @param context_styles [Hash] hash of the context styles that will be updated
84
+ # @param rule [Hash] rule from the STYLES_LIST to lookup in the context style for value removal
85
+ def remove_value(context_styles, rule)
86
+ if rule[:set] == :append_styles
87
+ context_styles[rule[:key]] -= rule[:values] if context_styles[:styles]
88
+ else
89
+ default = Context::DEFAULT_STYLES[rule[:key]]
90
+ default ? (context_styles[rule[:key]] = default) : context_styles.delete(rule[:key])
91
+ end
92
+ end
93
+
94
+ # Update context styles applying the initial rules (if set)
95
+ #
96
+ # @param context_styles [Hash] hash of the context styles that will be updated
97
+ #
98
+ # @return [Hash] the update context styles
99
+ def update_styles(context_styles)
100
+ initial.each do |rule|
101
+ next unless rule
102
+
103
+ remove_value(context_styles, rule)
104
+ end
105
+ context_styles
71
106
  end
72
107
 
73
108
  class << self
@@ -98,21 +133,34 @@ module PrawnHtml
98
133
 
99
134
  private
100
135
 
101
- def apply_rule!(result, rule, value)
102
- return unless rule
136
+ def process_styles(hash_styles, options:)
137
+ hash_styles.each do |key, value|
138
+ rule = evaluate_rule(key, value)
139
+ next unless rule
103
140
 
104
- if rule[:set] == :append_styles
105
- (result[rule[:key]] ||= []) << Utils.normalize_style(value)
106
- else
107
- result[rule[:key]] = Utils.send(rule[:set], value)
141
+ apply_rule!(merged_styles: @styles, rule: rule, value: value, options: options)
108
142
  end
143
+ @styles
109
144
  end
110
145
 
111
- def process_styles(hash_styles)
112
- hash_styles.each do |key, value|
113
- apply_rule!(@styles, STYLES_LIST[key], value)
146
+ def evaluate_rule(rule_key, attr_value)
147
+ key = nil
148
+ key = 'text-decoration-line-through' if rule_key == 'text-decoration' && attr_value == 'line-through'
149
+ key ||= rule_key
150
+ STYLES_LIST[key]
151
+ end
152
+
153
+ def apply_rule!(merged_styles:, rule:, value:, options:)
154
+ return (@initial << rule) if value == 'initial'
155
+
156
+ if rule[:set] == :append_styles
157
+ val = Utils.normalize_style(value, rule[:values])
158
+ (merged_styles[rule[:key]] ||= []) << val if val
159
+ else
160
+ opts = rule[:options] ? options[rule[:options]] : nil
161
+ val = Utils.send(rule[:set], value, options: opts)
162
+ merged_styles[rule[:key]] = val if val
114
163
  end
115
- @styles
116
164
  end
117
165
  end
118
166
  end
@@ -2,12 +2,12 @@
2
2
 
3
3
  module PrawnHtml
4
4
  module Callbacks
5
- class Highlight
5
+ class Background
6
6
  DEF_HIGHLIGHT = 'ffff00'
7
7
 
8
- def initialize(pdf, item)
8
+ def initialize(pdf, color = nil)
9
9
  @pdf = pdf
10
- @color = item.delete(:background) || DEF_HIGHLIGHT
10
+ @color = color || DEF_HIGHLIGHT
11
11
  end
12
12
 
13
13
  def render_behind(fragment)
@@ -2,15 +2,19 @@
2
2
 
3
3
  module PrawnHtml
4
4
  class Context < Array
5
- DEF_FONT_SIZE = 10.3
5
+ DEFAULT_STYLES = {
6
+ size: 16 * PX
7
+ }.freeze
6
8
 
7
- attr_accessor :last_margin, :last_text_node
9
+ attr_reader :previous_tag
10
+ attr_accessor :last_text_node
8
11
 
9
12
  # Init the Context
10
13
  def initialize(*_args)
11
14
  super
12
- @last_margin = 0
13
15
  @last_text_node = false
16
+ @merged_styles = nil
17
+ @previous_tag = nil
14
18
  end
15
19
 
16
20
  # Add an element to the context
@@ -25,6 +29,7 @@ module PrawnHtml
25
29
  element.parent = last
26
30
  push(element)
27
31
  element.on_context_add(self) if element.respond_to?(:on_context_add)
32
+ @merged_styles = nil
28
33
  self
29
34
  end
30
35
 
@@ -49,21 +54,32 @@ module PrawnHtml
49
54
  # Merge the context styles for text nodes
50
55
  #
51
56
  # @return [Hash] the hash of merged styles
52
- def text_node_styles
53
- each_with_object(base_styles) do |element, res|
54
- evaluate_element_styles(element, res)
55
- element.update_styles(res) if element.respond_to?(:update_styles)
56
- end
57
+ def merged_styles
58
+ @merged_styles ||=
59
+ each_with_object(DEFAULT_STYLES.dup) do |element, res|
60
+ evaluate_element_styles(element, res)
61
+ element.update_styles(res)
62
+ end
57
63
  end
58
64
 
59
- private
65
+ # Remove the last element from the context
66
+ def remove_last
67
+ last.on_context_remove(self) if last.respond_to?(:on_context_remove)
68
+ @merged_styles = nil
69
+ @last_text_node = false
70
+ @previous_tag = last
71
+ pop
72
+ end
60
73
 
61
- def base_styles
62
- {
63
- size: DEF_FONT_SIZE
64
- }
74
+ # White space is equal to 'pre'?
75
+ #
76
+ # @return [boolean] white space property of the last element is equal to 'pre'
77
+ def white_space_pre?
78
+ last && last.styles[:white_space] == :pre
65
79
  end
66
80
 
81
+ private
82
+
67
83
  def evaluate_element_styles(element, res)
68
84
  styles = element.styles.slice(*Attributes::STYLES_APPLY[:text_node])
69
85
  styles.each do |key, val|
@@ -11,6 +11,9 @@ module PrawnHtml
11
11
  def initialize(pdf)
12
12
  @buffer = []
13
13
  @context = Context.new
14
+ @last_margin = 0
15
+ @last_text = ''
16
+ @last_tag_open = false
14
17
  @pdf = pdf
15
18
  end
16
19
 
@@ -20,8 +23,9 @@ module PrawnHtml
20
23
  def on_tag_close(element)
21
24
  render_if_needed(element)
22
25
  apply_tag_close_styles(element)
23
- context.last_text_node = false
24
- context.pop
26
+ context.remove_last
27
+ @last_tag_open = false
28
+ @last_text = ''
25
29
  end
26
30
 
27
31
  # On tag open callback
@@ -35,8 +39,10 @@ module PrawnHtml
35
39
  tag_class = Tag.class_for(tag_name)
36
40
  return unless tag_class
37
41
 
38
- tag_class.new(tag_name, attributes: attributes, element_styles: element_styles).tap do |element|
39
- setup_element(element)
42
+ options = { width: pdf.page_width, height: pdf.page_height }
43
+ tag_class.new(tag_name, attributes: attributes, options: options).tap do |element|
44
+ setup_element(element, element_styles: element_styles)
45
+ @last_tag_open = true
40
46
  end
41
47
  end
42
48
 
@@ -46,11 +52,10 @@ module PrawnHtml
46
52
  #
47
53
  # @return [NilClass] nil value (=> no element)
48
54
  def on_text_node(content)
49
- return if content.match?(/\A\s*\Z/)
55
+ return if context.previous_tag&.block? && content.match?(/\A\s*\Z/)
50
56
 
51
- text = ::Oga::HTML::Entities.decode(context.before_content)
52
- text += content.gsub(/\A\s*\n\s*|\s*\n\s*\Z/, '').delete("\n").squeeze(' ')
53
- buffer << context.text_node_styles.merge(text: text)
57
+ text = prepare_text(content)
58
+ buffer << context.merged_styles.merge(text: text) unless text.empty?
54
59
  context.last_text_node = true
55
60
  nil
56
61
  end
@@ -61,26 +66,23 @@ module PrawnHtml
61
66
 
62
67
  output_content(buffer.dup, context.block_styles)
63
68
  buffer.clear
64
- context.last_margin = 0
69
+ @last_margin = 0
65
70
  end
66
71
 
67
72
  alias_method :flush, :render
68
73
 
69
74
  private
70
75
 
71
- attr_reader :buffer, :context, :pdf
76
+ attr_reader :buffer, :context, :last_margin, :pdf
72
77
 
73
- def setup_element(element)
74
- add_space_if_needed unless render_if_needed(element)
75
- apply_tag_open_styles(element)
78
+ def setup_element(element, element_styles:)
79
+ render_if_needed(element)
76
80
  context.add(element)
81
+ element.process_styles(element_styles: element_styles)
82
+ apply_tag_open_styles(element)
77
83
  element.custom_render(pdf, context) if element.respond_to?(:custom_render)
78
84
  end
79
85
 
80
- def add_space_if_needed
81
- buffer << SPACE if buffer.any? && !context.last_text_node && ![NEW_LINE, SPACE].include?(buffer.last)
82
- end
83
-
84
86
  def render_if_needed(element)
85
87
  render_needed = element&.block? && buffer.any? && buffer.last != NEW_LINE
86
88
  return false unless render_needed
@@ -91,39 +93,68 @@ module PrawnHtml
91
93
 
92
94
  def apply_tag_close_styles(element)
93
95
  tag_styles = element.tag_close_styles
94
- context.last_margin = tag_styles[:margin_bottom].to_f
95
- pdf.advance_cursor(context.last_margin + tag_styles[:padding_bottom].to_f)
96
+ @last_margin = tag_styles[:margin_bottom].to_f
97
+ pdf.advance_cursor(last_margin + tag_styles[:padding_bottom].to_f)
96
98
  pdf.start_new_page if tag_styles[:break_after]
97
99
  end
98
100
 
99
101
  def apply_tag_open_styles(element)
100
102
  tag_styles = element.tag_open_styles
101
- move_down = (tag_styles[:margin_top].to_f - context.last_margin) + tag_styles[:padding_top].to_f
103
+ move_down = (tag_styles[:margin_top].to_f - last_margin) + tag_styles[:padding_top].to_f
102
104
  pdf.advance_cursor(move_down) if move_down > 0
103
105
  pdf.start_new_page if tag_styles[:break_before]
104
106
  end
105
107
 
108
+ def prepare_text(content)
109
+ text = context.before_content ? ::Oga::HTML::Entities.decode(context.before_content) : ''
110
+ return (@last_text = text + content) if context.white_space_pre?
111
+
112
+ content = content.lstrip if @last_text[-1] == ' ' || @last_tag_open
113
+ text += content.tr("\n", ' ').squeeze(' ')
114
+ @last_text = text
115
+ end
116
+
106
117
  def output_content(buffer, block_styles)
107
118
  apply_callbacks(buffer)
108
119
  left_indent = block_styles[:margin_left].to_f + block_styles[:padding_left].to_f
109
- options = block_styles.slice(:align, :leading, :mode, :padding_left)
110
- options[:indent_paragraphs] = left_indent if left_indent > 0
111
- pdf.puts(buffer, options, bounding_box: bounds(block_styles))
120
+ options = block_styles.slice(:align, :indent_paragraphs, :leading, :mode, :padding_left)
121
+ options[:leading] = adjust_leading(buffer, options[:leading])
122
+ pdf.puts(buffer, options, bounding_box: bounds(buffer, options, block_styles), left_indent: left_indent)
112
123
  end
113
124
 
114
125
  def apply_callbacks(buffer)
115
126
  buffer.select { |item| item[:callback] }.each do |item|
116
- callback = Tag::CALLBACKS[item[:callback]]
117
- item[:callback] = callback.new(pdf, item)
127
+ callback, arg = item[:callback]
128
+ callback_class = Tag::CALLBACKS[callback]
129
+ item[:callback] = callback_class.new(pdf, arg)
130
+ end
131
+ end
132
+
133
+ def adjust_leading(buffer, leading)
134
+ return leading if leading
135
+
136
+ leadings = buffer.map do |item|
137
+ (item[:size] || Context::DEFAULT_STYLES[:size]) * (ADJUST_LEADING[item[:font]] || ADJUST_LEADING[nil])
118
138
  end
139
+ leadings.max.round(4)
119
140
  end
120
141
 
121
- def bounds(block_styles)
142
+ def bounds(buffer, options, block_styles)
122
143
  return unless block_styles[:position] == :absolute
123
144
 
124
- y = pdf.bounds.height - (block_styles[:top] || 0)
125
- w = pdf.bounds.width - (block_styles[:left] || 0)
126
- [[block_styles[:left] || 0, y], { width: w }]
145
+ x = if block_styles.include?(:right)
146
+ x1 = pdf.calc_buffer_width(buffer) + block_styles[:right]
147
+ x1 < pdf.page_width ? (pdf.page_width - x1) : 0
148
+ else
149
+ block_styles[:left] || 0
150
+ end
151
+ y = if block_styles.include?(:bottom)
152
+ pdf.calc_buffer_height(buffer, options) + block_styles[:bottom]
153
+ else
154
+ pdf.page_height - (block_styles[:top] || 0)
155
+ end
156
+
157
+ [[x, y], { width: pdf.page_width - x }]
127
158
  end
128
159
  end
129
160
  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,11 +2,16 @@
2
2
 
3
3
  module PrawnHtml
4
4
  class Tag
5
+ extend Forwardable
6
+
5
7
  CALLBACKS = {
6
- 'Highlight' => Callbacks::Highlight,
8
+ 'Background' => Callbacks::Background,
7
9
  'StrikeThrough' => Callbacks::StrikeThrough
8
10
  }.freeze
9
- TAG_CLASSES = %w[A B Blockquote Body Br Del Div H Hr I Img Li Mark Ol P Small Span U Ul].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
10
15
 
11
16
  attr_accessor :parent
12
17
  attr_reader :attrs, :tag
@@ -15,11 +20,11 @@ module PrawnHtml
15
20
  #
16
21
  # @param tag [Symbol] tag name
17
22
  # @param attributes [Hash] hash of element attributes
18
- # @param element_styles [String] document styles tp apply to the element
19
- def initialize(tag, attributes: {}, element_styles: '')
23
+ # @param options [Hash] options (container width/height/etc.)
24
+ def initialize(tag, attributes: {}, options: {})
20
25
  @tag = tag
26
+ @options = options
21
27
  @attrs = Attributes.new(attributes)
22
- process_styles(element_styles, attributes['style'])
23
28
  end
24
29
 
25
30
  # Is a block tag?
@@ -38,6 +43,16 @@ module PrawnHtml
38
43
  block_styles
39
44
  end
40
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
+
41
56
  # Styles to apply on tag closing
42
57
  #
43
58
  # @return [Hash] hash of styles to apply
@@ -45,13 +60,6 @@ module PrawnHtml
45
60
  styles.slice(*Attributes::STYLES_APPLY[:tag_close])
46
61
  end
47
62
 
48
- # Styles hash
49
- #
50
- # @return [Hash] hash of styles
51
- def styles
52
- attrs.styles
53
- end
54
-
55
63
  # Styles to apply on tag opening
56
64
  #
57
65
  # @return [Hash] hash of styles to apply
@@ -77,10 +85,6 @@ module PrawnHtml
77
85
 
78
86
  private
79
87
 
80
- def process_styles(element_styles, inline_styles)
81
- attrs.merge_text_styles!(tag_styles) if respond_to?(:tag_styles)
82
- attrs.merge_text_styles!(element_styles)
83
- attrs.merge_text_styles!(inline_styles)
84
- end
88
+ attr_reader :options
85
89
  end
86
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
- "href: #{attrs.href}" if attrs.href
13
+ <<~STYLES
14
+ color: #00E;
15
+ text-decoration: underline;
16
+ STYLES
10
17
  end
11
18
  end
12
19
  end
@@ -5,9 +5,9 @@ module PrawnHtml
5
5
  class Blockquote < Tag
6
6
  ELEMENTS = [:blockquote].freeze
7
7
 
8
- MARGIN_BOTTOM = 10
9
- MARGIN_LEFT = 25
10
- MARGIN_TOP = 10
8
+ MARGIN_BOTTOM = 12.7
9
+ MARGIN_LEFT = 40.4
10
+ MARGIN_TOP = 12.7
11
11
 
12
12
  def block?
13
13
  true
@@ -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.is_a?(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,7 +6,7 @@ module PrawnHtml
6
6
  ELEMENTS = [:del, :s].freeze
7
7
 
8
8
  def tag_styles
9
- 'callback: StrikeThrough'
9
+ 'text-decoration: line-through'
10
10
  end
11
11
  end
12
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?
@@ -12,16 +12,17 @@ module PrawnHtml
12
12
  def custom_render(pdf, context)
13
13
  parsed_styles = Attributes.parse_styles(attrs.style)
14
14
  block_styles = context.block_styles
15
- evaluated_styles = evaluate_styles(pdf, block_styles.merge(parsed_styles))
15
+ evaluated_styles = adjust_styles(pdf, block_styles.merge(parsed_styles))
16
16
  pdf.image(@attrs.src, evaluated_styles)
17
17
  end
18
18
 
19
19
  private
20
20
 
21
- def evaluate_styles(pdf, img_styles)
21
+ def adjust_styles(pdf, img_styles)
22
22
  {}.tap do |result|
23
- result[:width] = Utils.convert_size(img_styles['width'], pdf.bounds.width) if img_styles.include?('width')
24
- result[:height] = Utils.convert_size(img_styles['height'], pdf.bounds.height) if img_styles.include?('height')
23
+ w, h = img_styles['width'], img_styles['height']
24
+ result[:width] = Utils.convert_size(w, options: pdf.page_width) if w
25
+ result[:height] = Utils.convert_size(h, options: pdf.page_height) if h
25
26
  result[:position] = img_styles[:align] if %i[left center right].include?(img_styles[:align])
26
27
  end
27
28
  end
@@ -5,6 +5,9 @@ module PrawnHtml
5
5
  class Li < Tag
6
6
  ELEMENTS = [:li].freeze
7
7
 
8
+ INDENT_OL = -12
9
+ INDENT_UL = -6
10
+
8
11
  def block?
9
12
  true
10
13
  end
@@ -13,9 +16,21 @@ module PrawnHtml
13
16
  @counter ? "#{@counter}. " : "#{@symbol} "
14
17
  end
15
18
 
19
+ def block_styles
20
+ super.tap do |bs|
21
+ bs[:indent_paragraphs] = @indent
22
+ end
23
+ end
24
+
16
25
  def on_context_add(_context)
17
- @counter = (parent.counter += 1) if parent.is_a? Ol
18
- @symbol = parent.styles[:list_style_type] || '&bullet;' if parent.is_a? Ul
26
+ case parent.class.to_s
27
+ when 'PrawnHtml::Tags::Ol'
28
+ @indent = INDENT_OL
29
+ @counter = (parent.counter += 1)
30
+ when 'PrawnHtml::Tags::Ul'
31
+ @indent = INDENT_UL
32
+ @symbol = parent.styles[:list_style_type] || '&bullet;'
33
+ end
19
34
  end
20
35
  end
21
36
  end
@@ -6,7 +6,7 @@ module PrawnHtml
6
6
  ELEMENTS = [:mark].freeze
7
7
 
8
8
  def tag_styles
9
- 'callback: Highlight'
9
+ 'background: #ff0'
10
10
  end
11
11
  end
12
12
  end
@@ -5,21 +5,38 @@ module PrawnHtml
5
5
  class Ol < Tag
6
6
  ELEMENTS = [:ol].freeze
7
7
 
8
- MARGIN_LEFT = 25
8
+ MARGIN_TOP = 15
9
+ MARGIN_LEFT = 40
10
+ MARGIN_BOTTOM = 15
9
11
 
10
12
  attr_accessor :counter
11
13
 
12
- def initialize(tag, attributes: {}, element_styles: '')
14
+ def initialize(tag, attributes: {}, options: {})
13
15
  super
14
16
  @counter = 0
17
+ @first_level = false
15
18
  end
16
19
 
17
20
  def block?
18
21
  true
19
22
  end
20
23
 
24
+ def on_context_add(context)
25
+ return if context.map(&:tag).count { |el| el == :ol } > 1
26
+
27
+ @first_level = true
28
+ end
29
+
21
30
  def tag_styles
22
- "margin-left: #{MARGIN_LEFT}px"
31
+ if @first_level
32
+ <<~STYLES
33
+ margin-top: #{MARGIN_TOP}px;
34
+ margin-left: #{MARGIN_LEFT}px;
35
+ margin-bottom: #{MARGIN_BOTTOM}px;
36
+ STYLES
37
+ else
38
+ "margin-left: #{MARGIN_LEFT}px"
39
+ end
23
40
  end
24
41
  end
25
42
  end
@@ -5,8 +5,8 @@ module PrawnHtml
5
5
  class P < Tag
6
6
  ELEMENTS = [:p].freeze
7
7
 
8
- MARGIN_BOTTOM = 6
9
- MARGIN_TOP = 6
8
+ MARGIN_BOTTOM = 12.5
9
+ MARGIN_TOP = 12.5
10
10
 
11
11
  def block?
12
12
  true
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ module Tags
5
+ class Pre < Tag
6
+ ELEMENTS = [:pre].freeze
7
+
8
+ MARGIN_BOTTOM = 14
9
+ MARGIN_TOP = 14
10
+
11
+ def block?
12
+ true
13
+ end
14
+
15
+ def tag_styles
16
+ <<~STYLES
17
+ font-family: Courier;
18
+ margin-bottom: #{MARGIN_BOTTOM}px;
19
+ margin-top: #{MARGIN_TOP}px;
20
+ white-space: pre;
21
+ STYLES
22
+ end
23
+ end
24
+ end
25
+ end
@@ -5,10 +5,10 @@ module PrawnHtml
5
5
  class Small < Tag
6
6
  ELEMENTS = [:small].freeze
7
7
 
8
- def update_styles(styles)
9
- size = (styles[:size] || Context::DEF_FONT_SIZE) * 0.85
10
- styles[:size] = size
11
- styles
8
+ def update_styles(context_styles)
9
+ size = (context_styles[:size] || Context::DEFAULT_STYLES[:size]) * 0.85
10
+ context_styles[:size] = size
11
+ super(context_styles)
12
12
  end
13
13
  end
14
14
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ module Tags
5
+ class Sub < Tag
6
+ ELEMENTS = [:sub].freeze
7
+
8
+ def tag_styles
9
+ 'vertical-align: sub'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ module Tags
5
+ class Sup < Tag
6
+ ELEMENTS = [:sup].freeze
7
+
8
+ def tag_styles
9
+ 'vertical-align: super'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -5,14 +5,35 @@ module PrawnHtml
5
5
  class Ul < Tag
6
6
  ELEMENTS = [:ul].freeze
7
7
 
8
- MARGIN_LEFT = 25
8
+ MARGIN_TOP = 15
9
+ MARGIN_LEFT = 40
10
+ MARGIN_BOTTOM = 15
11
+
12
+ def initialize(tag, attributes: {}, options: {})
13
+ super
14
+ @first_level = false
15
+ end
9
16
 
10
17
  def block?
11
18
  true
12
19
  end
13
20
 
21
+ def on_context_add(context)
22
+ return if context.map(&:tag).count { |el| el == :ul } > 1
23
+
24
+ @first_level = true
25
+ end
26
+
14
27
  def tag_styles
15
- "margin-left: #{MARGIN_LEFT}px"
28
+ if @first_level
29
+ <<~STYLES
30
+ margin-top: #{MARGIN_TOP}px;
31
+ margin-left: #{MARGIN_LEFT}px;
32
+ margin-bottom: #{MARGIN_BOTTOM}px;
33
+ STYLES
34
+ else
35
+ "margin-left: #{MARGIN_LEFT}px"
36
+ end
16
37
  end
17
38
  end
18
39
  end
@@ -5,9 +5,29 @@ module PrawnHtml
5
5
  NORMALIZE_STYLES = {
6
6
  'bold' => :bold,
7
7
  'italic' => :italic,
8
+ 'sub' => :subscript,
9
+ 'super' => :superscript,
8
10
  'underline' => :underline
9
11
  }.freeze
10
12
 
13
+ # Setup a background callback
14
+ #
15
+ # @param value [String] HTML string color
16
+ #
17
+ # @return [Array] callback name and argument value
18
+ def callback_background(value, options: nil)
19
+ ['Background', convert_color(value, options: options)]
20
+ end
21
+
22
+ # Setup a strike through callback
23
+ #
24
+ # @param value [String] unused
25
+ #
26
+ # @return [Array] callback name and argument value
27
+ def callback_strike_through(value, options: nil)
28
+ ['StrikeThrough', nil]
29
+ end
30
+
11
31
  # Converts a color string
12
32
  #
13
33
  # Supported formats:
@@ -19,7 +39,7 @@ module PrawnHtml
19
39
  # @param value [String] HTML string color
20
40
  #
21
41
  # @return [String] adjusted string color or nil if value is invalid
22
- def convert_color(value)
42
+ def convert_color(value, options: nil)
23
43
  val = value.to_s.strip.downcase
24
44
  return Regexp.last_match[1] if val.match /\A#([a-f0-9]{6})\Z/ # rubocop:disable Performance/RedundantMatch
25
45
 
@@ -40,7 +60,7 @@ module PrawnHtml
40
60
  # @param value [String] string decimal
41
61
  #
42
62
  # @return [Float] converted and rounded float number
43
- def convert_float(value)
63
+ def convert_float(value, options: nil)
44
64
  val = value&.gsub(/[^0-9.]/, '') || ''
45
65
  val.to_f.round(4)
46
66
  end
@@ -48,14 +68,14 @@ module PrawnHtml
48
68
  # Converts a size string
49
69
  #
50
70
  # @param value [String] size string
51
- # @param container_size [Numeric] container size
71
+ # @param options [Numeric] container size
52
72
  #
53
73
  # @return [Float] converted and rounded size
54
- def convert_size(value, container_size = nil)
74
+ def convert_size(value, options: nil)
55
75
  val = value&.gsub(/[^0-9.]/, '') || ''
56
76
  val =
57
- if container_size && value.include?('%')
58
- val.to_f * container_size * 0.01
77
+ if options && value&.include?('%')
78
+ val.to_f * options * 0.01
59
79
  else
60
80
  val.to_f * PrawnHtml::PX
61
81
  end
@@ -67,7 +87,7 @@ module PrawnHtml
67
87
  # @param value [String] string
68
88
  #
69
89
  # @return [Symbol] symbol
70
- def convert_symbol(value)
90
+ def convert_symbol(value, options: nil)
71
91
  value.to_sym if value && !value.match?(/\A\s*\Z/)
72
92
  end
73
93
 
@@ -76,18 +96,20 @@ module PrawnHtml
76
96
  # @param value
77
97
  #
78
98
  # @return value
79
- def copy_value(value)
99
+ def copy_value(value, options: nil)
80
100
  value
81
101
  end
82
102
 
83
103
  # Normalize a style value
84
104
  #
85
105
  # @param value [String] string value
106
+ # @param accepted_values [Array] allowlist of valid values (symbols)
86
107
  #
87
108
  # @return [Symbol] style value or nil
88
- def normalize_style(value)
109
+ def normalize_style(value, accepted_values)
89
110
  val = value&.strip&.downcase
90
- NORMALIZE_STYLES[val]
111
+ ret = NORMALIZE_STYLES[val]
112
+ accepted_values.include?(ret) ? ret : nil
91
113
  end
92
114
 
93
115
  # Unquotes a string
@@ -95,13 +117,13 @@ module PrawnHtml
95
117
  # @param value [String] string
96
118
  #
97
119
  # @return [String] string without quotes at the beginning/ending
98
- def unquote(value)
120
+ def unquote(value, options: nil)
99
121
  (value&.strip || +'').tap do |val|
100
122
  val.gsub!(/\A['"]|["']\Z/, '')
101
123
  end
102
124
  end
103
125
 
104
- module_function :convert_color, :convert_float, :convert_size, :convert_symbol, :copy_value, :normalize_style,
105
- :unquote
126
+ module_function :callback_background, :callback_strike_through, :convert_color, :convert_float, :convert_size,
127
+ :convert_symbol, :copy_value, :normalize_style, :unquote
106
128
  end
107
129
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PrawnHtml # :nodoc:
4
- VERSION = '0.4.0'
4
+ VERSION = '0.6.2'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prawn-html
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mattia Roccoberton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-29 00:00:00.000000000 Z
11
+ date: 2021-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oga
@@ -48,7 +48,7 @@ files:
48
48
  - README.md
49
49
  - lib/prawn-html.rb
50
50
  - lib/prawn_html/attributes.rb
51
- - lib/prawn_html/callbacks/highlight.rb
51
+ - lib/prawn_html/callbacks/background.rb
52
52
  - lib/prawn_html/callbacks/strike_through.rb
53
53
  - lib/prawn_html/context.rb
54
54
  - lib/prawn_html/document_renderer.rb
@@ -60,6 +60,7 @@ files:
60
60
  - lib/prawn_html/tags/blockquote.rb
61
61
  - lib/prawn_html/tags/body.rb
62
62
  - lib/prawn_html/tags/br.rb
63
+ - lib/prawn_html/tags/code.rb
63
64
  - lib/prawn_html/tags/del.rb
64
65
  - lib/prawn_html/tags/div.rb
65
66
  - lib/prawn_html/tags/h.rb
@@ -70,8 +71,11 @@ files:
70
71
  - lib/prawn_html/tags/mark.rb
71
72
  - lib/prawn_html/tags/ol.rb
72
73
  - lib/prawn_html/tags/p.rb
74
+ - lib/prawn_html/tags/pre.rb
73
75
  - lib/prawn_html/tags/small.rb
74
76
  - lib/prawn_html/tags/span.rb
77
+ - lib/prawn_html/tags/sub.rb
78
+ - lib/prawn_html/tags/sup.rb
75
79
  - lib/prawn_html/tags/u.rb
76
80
  - lib/prawn_html/tags/ul.rb
77
81
  - lib/prawn_html/utils.rb