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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 998ac3f3429812cc60dfe2bdf0184a86c116b1b80c7522376f99e044f185207c
4
- data.tar.gz: 1a4ce4f9055fa8dd8ed121665102698b6077d69986974c366098d69759555d80
3
+ metadata.gz: 3b659b809526c4c961782f613e6946613232dd836ac056ca48d3dabebdbf9a02
4
+ data.tar.gz: 2549b8b8b872b44f3d6f424236249e7795f97afeadf95299bf06836fb979a13a
5
5
  SHA512:
6
- metadata.gz: d443116a4be710f698f02da8092b19b0d83f9b319c0e7b431ae9cfe67d11c208ddea48ff5c995057bd3b28056dafcb2ff8461c1ad3be8134f9dcaa926b96ef73
7
- data.tar.gz: 530a30ff700f75312a5f72b38ef433f9f03a3eab9ac9d83297a3eacc20d680df4ab1f8a4bc3c5134146b3da71be6ffa23d151bc71824dc21022fe7c5aa592758
6
+ metadata.gz: b58a21e4424c89db5ff388ab2507ec502ea283ba9e6234d46f3688e477ba21944ee72f813cc9ddb4454426f5e4cc7397531ffa34c74eeb7795a0bbbeaa8d34ed
7
+ data.tar.gz: 0e3cfad427968577135c8c8791f698b3c5d6e95463b9364d0630d9caf87407f378c3fb725bfe11349cad210d1d0c3bba1a90555ebb9219c620e674fff0413ff8
data/README.md CHANGED
@@ -39,7 +39,9 @@ HTML tags:
39
39
 
40
40
  - **a**: link
41
41
  - **b**: bold
42
+ - **blockquote**: block quotation element
42
43
  - **br**: new line
44
+ - **code**: inline code element
43
45
  - **del**: strike-through
44
46
  - **div**: block element
45
47
  - **em**: italic
@@ -52,10 +54,13 @@ HTML tags:
52
54
  - **mark**: highlight
53
55
  - **ol**: ordered list
54
56
  - **p**: block element
57
+ - **pre**: preformatted text element
55
58
  - **s**: strike-through
56
59
  - **small**: smaller text
57
60
  - **span**: inline element
58
61
  - **strong**: bold
62
+ - **sub**: subscript element
63
+ - **sup**: superscript element
59
64
  - **u**: underline
60
65
  - **ul**: unordered list
61
66
 
@@ -85,6 +90,8 @@ CSS attributes (dimensional units are ignored and considered in pixel):
85
90
  - **top**: see *position (absolute)*
86
91
  - **width**: for *img* tag, support also percentage, ex. `<img src="image.jpg" style="width: 50%; height: 200px"/>`
87
92
 
93
+ The above attributes supports the `initial` value to reset them to their original value.
94
+
88
95
  For colors, the supported formats are:
89
96
  - 3 hex digits, ex. `color: #FB1`;
90
97
  - 6 hex digits, ex. `color: #abcdef`;
@@ -100,8 +107,7 @@ Some custom data attributes are used to pass options:
100
107
 
101
108
  ## Document styles
102
109
 
103
- [Experimental feature] You can define document CSS rules inside an _head_ tag, but with a limited support for now.
104
- Only single CSS selectors and basic ones are supported. Example:
110
+ You can define document CSS rules inside an _head_ tag. Example:
105
111
 
106
112
  ```html
107
113
  <!DOCTYPE html>
data/lib/prawn-html.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PrawnHtml
4
- PX = 0.66 # conversion constant for pixel sixes
4
+ PX = 0.6 # conversion constant for pixel sixes
5
5
 
6
6
  COLORS = {
7
7
  'aliceblue' => 'f0f8ff',
@@ -168,9 +168,10 @@ require 'prawn'
168
168
 
169
169
  require 'prawn_html/utils'
170
170
 
171
+ Dir["#{__dir__}/prawn_html/callbacks/*.rb"].sort.each { |f| require f }
172
+
171
173
  require 'prawn_html/tag'
172
174
  Dir["#{__dir__}/prawn_html/tags/*.rb"].sort.each { |f| require f }
173
- Dir["#{__dir__}/prawn_html/callbacks/*.rb"].sort.each { |f| require f }
174
175
 
175
176
  require 'prawn_html/attributes'
176
177
  require 'prawn_html/context'
@@ -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_symbol },
24
- 'font-weight' => { key: :styles, set: :append_symbol },
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_symbol },
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,10 +56,7 @@ module PrawnHtml
50
56
  def initialize(attributes = {})
51
57
  super
52
58
  @styles = {} # result styles
53
- return unless style
54
-
55
- styles_hash = Attributes.parse_styles(style)
56
- process_styles(styles_hash)
59
+ @initial = Set.new
57
60
  end
58
61
 
59
62
  # Processes the data attributes
@@ -66,21 +69,40 @@ module PrawnHtml
66
69
  end
67
70
  end
68
71
 
69
- # Merge already parsed styles
72
+ # Merge text styles
70
73
  #
71
- # @param parsed_styles [Hash] hash of parsed styles
72
- def merge_styles!(parsed_styles)
73
- @styles.merge!(parsed_styles)
74
+ # @param text_styles [String] styles to parse and process
75
+ # @param options [Hash] options (container width/height/etc.)
76
+ def merge_text_styles!(text_styles, options: {})
77
+ hash_styles = Attributes.parse_styles(text_styles)
78
+ process_styles(hash_styles, options: options) unless hash_styles.empty?
74
79
  end
75
80
 
76
- # Processes the styles attributes
81
+ # Remove an attribute value from the context styles
77
82
  #
78
- # @param styles_hash [Hash] hash of styles attributes
79
- def process_styles(styles_hash)
80
- styles_hash.each do |key, value|
81
- apply_rule!(@styles, STYLES_LIST[key], value)
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])
82
91
  end
83
- @styles
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
84
106
  end
85
107
 
86
108
  class << self
@@ -111,13 +133,33 @@ module PrawnHtml
111
133
 
112
134
  private
113
135
 
114
- def apply_rule!(result, rule, value)
115
- 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
140
+
141
+ apply_rule!(merged_styles: @styles, rule: rule, value: value, options: options)
142
+ end
143
+ @styles
144
+ end
145
+
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'
116
155
 
117
- if rule[:set] == :append_symbol
118
- (result[rule[:key]] ||= []) << Utils.convert_symbol(value)
156
+ if rule[:set] == :append_styles
157
+ val = Utils.normalize_style(value)
158
+ (merged_styles[rule[:key]] ||= []) << val if val
119
159
  else
120
- result[rule[:key]] = Utils.send(rule[:set], value)
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
121
163
  end
122
164
  end
123
165
  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
 
@@ -32,9 +37,7 @@ module PrawnHtml
32
37
  #
33
38
  # @return [String] before content string
34
39
  def before_content
35
- return '' if empty? || !last.respond_to?(:tag_styles)
36
-
37
- last.tag_styles[:before_content].to_s
40
+ (last.respond_to?(:before_content) && last.before_content) || ''
38
41
  end
39
42
 
40
43
  # Merges the context block styles
@@ -51,21 +54,25 @@ module PrawnHtml
51
54
  # Merge the context styles for text nodes
52
55
  #
53
56
  # @return [Hash] the hash of merged styles
54
- def text_node_styles
55
- each_with_object(base_styles) do |element, res|
56
- evaluate_element_styles(element, res)
57
- element.update_styles(res) if element.respond_to?(:update_styles)
58
- 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
59
63
  end
60
64
 
61
- private
62
-
63
- def base_styles
64
- {
65
- size: DEF_FONT_SIZE
66
- }
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.tag
71
+ pop
67
72
  end
68
73
 
74
+ private
75
+
69
76
  def evaluate_element_styles(element, res)
70
77
  styles = element.styles.slice(*Attributes::STYLES_APPLY[:text_node])
71
78
  styles.each do |key, val|
@@ -11,43 +11,33 @@ module PrawnHtml
11
11
  def initialize(pdf)
12
12
  @buffer = []
13
13
  @context = Context.new
14
- @document_styles = {}
14
+ @last_margin = 0
15
15
  @pdf = pdf
16
16
  end
17
17
 
18
- # Evaluate the document styles and store the internally
19
- #
20
- # @param styles [Hash] styles hash with CSS selectors as keys and rules as values
21
- def assign_document_styles(styles)
22
- @document_styles.merge!(
23
- styles.transform_values do |style_rules|
24
- Attributes.new(style: style_rules).styles
25
- end
26
- )
27
- end
28
-
29
18
  # On tag close callback
30
19
  #
31
20
  # @param element [Tag] closing element wrapper
32
21
  def on_tag_close(element)
33
22
  render_if_needed(element)
34
23
  apply_tag_close_styles(element)
35
- context.last_text_node = false
36
- context.pop
24
+ context.remove_last
37
25
  end
38
26
 
39
27
  # On tag open callback
40
28
  #
41
29
  # @param tag_name [String] the tag name of the opening element
42
30
  # @param attributes [Hash] an hash of the element attributes
31
+ # @param element_styles [String] document styles to apply to the element
43
32
  #
44
33
  # @return [Tag] the opening element wrapper
45
- def on_tag_open(tag_name, attributes)
34
+ def on_tag_open(tag_name, attributes:, element_styles: '')
46
35
  tag_class = Tag.class_for(tag_name)
47
36
  return unless tag_class
48
37
 
49
- tag_class.new(tag_name, attributes, document_styles).tap do |element|
50
- setup_element(element)
38
+ options = { width: pdf.page_width, height: pdf.page_height }
39
+ tag_class.new(tag_name, attributes: attributes, options: options).tap do |element|
40
+ setup_element(element, element_styles: element_styles)
51
41
  end
52
42
  end
53
43
 
@@ -59,9 +49,7 @@ module PrawnHtml
59
49
  def on_text_node(content)
60
50
  return if content.match?(/\A\s*\Z/)
61
51
 
62
- text = ::Oga::HTML::Entities.decode(context.before_content)
63
- text += content.gsub(/\A\s*\n\s*|\s*\n\s*\Z/, '').delete("\n").squeeze(' ')
64
- buffer << context.text_node_styles.merge(text: text)
52
+ buffer << context.merged_styles.merge(text: prepare_text(content))
65
53
  context.last_text_node = true
66
54
  nil
67
55
  end
@@ -72,19 +60,20 @@ module PrawnHtml
72
60
 
73
61
  output_content(buffer.dup, context.block_styles)
74
62
  buffer.clear
75
- context.last_margin = 0
63
+ @last_margin = 0
76
64
  end
77
65
 
78
66
  alias_method :flush, :render
79
67
 
80
68
  private
81
69
 
82
- attr_reader :buffer, :context, :document_styles, :pdf
70
+ attr_reader :buffer, :context, :last_margin, :pdf
83
71
 
84
- def setup_element(element)
72
+ def setup_element(element, element_styles:)
85
73
  add_space_if_needed unless render_if_needed(element)
86
- apply_tag_open_styles(element)
87
74
  context.add(element)
75
+ element.process_styles(element_styles: element_styles)
76
+ apply_tag_open_styles(element)
88
77
  element.custom_render(pdf, context) if element.respond_to?(:custom_render)
89
78
  end
90
79
 
@@ -102,32 +91,63 @@ module PrawnHtml
102
91
 
103
92
  def apply_tag_close_styles(element)
104
93
  tag_styles = element.tag_close_styles
105
- context.last_margin = tag_styles[:margin_bottom].to_f
106
- pdf.advance_cursor(context.last_margin + tag_styles[:padding_bottom].to_f)
94
+ @last_margin = tag_styles[:margin_bottom].to_f
95
+ pdf.advance_cursor(last_margin + tag_styles[:padding_bottom].to_f)
107
96
  pdf.start_new_page if tag_styles[:break_after]
108
97
  end
109
98
 
110
99
  def apply_tag_open_styles(element)
111
100
  tag_styles = element.tag_open_styles
112
- move_down = (tag_styles[:margin_top].to_f - context.last_margin) + tag_styles[:padding_top].to_f
101
+ move_down = (tag_styles[:margin_top].to_f - last_margin) + tag_styles[:padding_top].to_f
113
102
  pdf.advance_cursor(move_down) if move_down > 0
114
103
  pdf.start_new_page if tag_styles[:break_before]
115
104
  end
116
105
 
106
+ def prepare_text(content)
107
+ white_space_pre = context.last && context.last.styles[:white_space] == :pre
108
+ text = ::Oga::HTML::Entities.decode(context.before_content)
109
+ text += white_space_pre ? content : content.gsub(/\A\s*\n\s*|\s*\n\s*\Z/, '').delete("\n").squeeze(' ')
110
+ text
111
+ end
112
+
117
113
  def output_content(buffer, block_styles)
118
- buffer.each { |item| item[:callback] = item[:callback].new(pdf, item) if item[:callback] }
114
+ apply_callbacks(buffer)
119
115
  left_indent = block_styles[:margin_left].to_f + block_styles[:padding_left].to_f
120
- options = block_styles.slice(:align, :leading, :mode, :padding_left)
121
- options[:indent_paragraphs] = left_indent if left_indent > 0
122
- pdf.puts(buffer, options, bounding_box: bounds(block_styles))
116
+ options = block_styles.slice(:align, :indent_paragraphs, :leading, :mode, :padding_left)
117
+ options[:leading] = adjust_leading(buffer, options[:leading])
118
+ pdf.puts(buffer, options, bounding_box: bounds(buffer, options, block_styles), left_indent: left_indent)
119
+ end
120
+
121
+ def apply_callbacks(buffer)
122
+ buffer.select { |item| item[:callback] }.each do |item|
123
+ callback, arg = item[:callback]
124
+ callback_class = Tag::CALLBACKS[callback]
125
+ item[:callback] = callback_class.new(pdf, arg)
126
+ end
127
+ end
128
+
129
+ def adjust_leading(buffer, leading)
130
+ return leading if leading
131
+
132
+ (buffer.map { |item| item[:size] || Context::DEFAULT_STYLES[:size] }.max * 0.055).round(4)
123
133
  end
124
134
 
125
- def bounds(block_styles)
135
+ def bounds(buffer, options, block_styles)
126
136
  return unless block_styles[:position] == :absolute
127
137
 
128
- y = pdf.bounds.height - (block_styles[:top] || 0)
129
- w = pdf.bounds.width - (block_styles[:left] || 0)
130
- [[block_styles[:left] || 0, y], { width: w }]
138
+ x = if block_styles.include?(:right)
139
+ x1 = pdf.calc_buffer_width(buffer) + block_styles[:right]
140
+ x1 < pdf.page_width ? (pdf.page_width - x1) : 0
141
+ else
142
+ block_styles[:left] || 0
143
+ end
144
+ y = if block_styles.include?(:bottom)
145
+ pdf.calc_buffer_height(buffer, options) + block_styles[:bottom]
146
+ else
147
+ pdf.page_height - (block_styles[:top] || 0)
148
+ end
149
+
150
+ [[x, y], { width: pdf.page_width - x }]
131
151
  end
132
152
  end
133
153
  end