prawn-html 0.4.0 → 0.6.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.
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