prawn-html 0.3.2 → 0.6.0

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: 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