prawn-html 0.3.0 → 0.5.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: c519c9608664ef77b5b4489752f49d29ae4dfda7ee42e43685ee5951524bd1e3
4
- data.tar.gz: c768fa73bc5601bec246a963153a3d20f0d6712fa72dea6441a27495dc374cee
3
+ metadata.gz: e82e0a464e39fd4c47c399cb043ceefe090524b29ead1c06795a484d4c648388
4
+ data.tar.gz: e124a7fb9800a434f42d403b478c6a6cb30b4e7d1e7e540d015028dec6daaa59
5
5
  SHA512:
6
- metadata.gz: b62ed1e07608cdb62829fe50ae708c16d55b1510c49ff8e02eb47442dca105cfeb2b18e751dd5f84e421687ba970fecb3078db904d3fc867af93ecbe67361e07
7
- data.tar.gz: 503c65d6d22d496c634634dca3c57eda3e864ce91448ad2c119e0cb89857e7731cdef7fd209a1f48d8bda82ad0dc8eff724e407a957bd4d8323f410385ebc906
6
+ metadata.gz: c08e1252e2c9c8f1591840179549d3e14483d91361b1a9f56d96f23cfa30829b98d8d2ace33808a73a9467e1ff37054d7f462d1c7170db273a4ff83b203d807f
7
+ data.tar.gz: '0979bd9a66e463ee8e4ea3c141a2d4e2fcc0e3b1bc82397aeda2e357c10c4518b00b1e1e4fc3f2ec786070c6d229241423cd5933a89fbc9137bdccc211d22a44'
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,16 +54,21 @@ 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
 
62
67
  CSS attributes (dimensional units are ignored and considered in pixel):
63
68
 
64
69
  - **background**: for *mark* tag (3/6 hex digits or RGB or color name), ex. `style="background: #FECD08"`
70
+ - **break-after**: go to a new page after some elements, ex. `style="break-after: auto"`
71
+ - **break-before**: go to a new page before some elements, ex. `style="break-before: auto"`
65
72
  - **color**: (3/6 hex digits or RGB or color name) ex. `style="color: #FB1"`
66
73
  - **font-family**: font must be registered, quotes are optional, ex. `style="font-family: Courier"`
67
74
  - **font-size**: ex. `style="font-size: 20px"`
@@ -98,8 +105,7 @@ Some custom data attributes are used to pass options:
98
105
 
99
106
  ## Document styles
100
107
 
101
- [Experimental feature] You can define document CSS rules inside an _head_ tag, but with a limited support for now.
102
- Only single CSS selectors and basic ones are supported. Example:
108
+ You can define document CSS rules inside an _head_ tag. Example:
103
109
 
104
110
  ```html
105
111
  <!DOCTYPE html>
data/lib/prawn-html.rb CHANGED
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'prawn'
4
-
5
3
  module PrawnHtml
6
- PX = 0.66 # conversion constant for pixel sixes
4
+ PX = 0.6 # conversion constant for pixel sixes
7
5
 
8
6
  COLORS = {
9
7
  'aliceblue' => 'f0f8ff',
@@ -157,20 +155,26 @@ module PrawnHtml
157
155
  }.freeze
158
156
 
159
157
  def append_html(pdf, html)
160
- html_parser = PrawnHtml::HtmlParser.new(pdf)
158
+ pdf_wrapper = PdfWrapper.new(pdf)
159
+ renderer = DocumentRenderer.new(pdf_wrapper)
160
+ html_parser = PrawnHtml::HtmlParser.new(renderer)
161
161
  html_parser.process(html)
162
162
  end
163
163
 
164
164
  module_function :append_html
165
165
  end
166
166
 
167
+ require 'prawn'
168
+
167
169
  require 'prawn_html/utils'
168
170
 
171
+ Dir["#{__dir__}/prawn_html/callbacks/*.rb"].sort.each { |f| require f }
172
+
169
173
  require 'prawn_html/tag'
170
174
  Dir["#{__dir__}/prawn_html/tags/*.rb"].sort.each { |f| require f }
171
- Dir["#{__dir__}/prawn_html/callbacks/*.rb"].sort.each { |f| require f }
172
175
 
173
176
  require 'prawn_html/attributes'
174
177
  require 'prawn_html/context'
178
+ require 'prawn_html/pdf_wrapper'
175
179
  require 'prawn_html/document_renderer'
176
180
  require 'prawn_html/html_parser'
@@ -7,39 +7,44 @@ module PrawnHtml
7
7
  attr_reader :styles
8
8
 
9
9
  STYLES_APPLY = {
10
- block: %i[align leading left margin_left padding_left position top],
11
- tag_close: %i[margin_bottom padding_bottom],
12
- tag_open: %i[margin_top padding_top],
13
- text_node: %i[background callback character_spacing color font link list_style_type size styles]
10
+ block: %i[align bottom leading left margin_left padding_left position right top],
11
+ tag_close: %i[margin_bottom padding_bottom break_after],
12
+ tag_open: %i[margin_top padding_top break_before],
13
+ text_node: %i[callback character_spacing color font link list_style_type size styles white_space]
14
14
  }.freeze
15
15
 
16
16
  STYLES_LIST = {
17
17
  # text node styles
18
- 'background' => { key: :background, set: :convert_color },
19
- 'callback' => { key: :callback, set: :copy_value },
18
+ 'background' => { key: :callback, set: :callback_background },
20
19
  'color' => { key: :color, set: :convert_color },
21
20
  'font-family' => { key: :font, set: :unquote },
22
21
  'font-size' => { key: :size, set: :convert_size },
23
- 'font-style' => { key: :styles, set: :append_symbol },
24
- 'font-weight' => { key: :styles, set: :append_symbol },
22
+ 'font-style' => { key: :styles, set: :append_styles },
23
+ 'font-weight' => { key: :styles, set: :append_styles },
25
24
  'href' => { key: :link, set: :copy_value },
26
25
  'letter-spacing' => { key: :character_spacing, set: :convert_float },
27
26
  'list-style-type' => { key: :list_style_type, set: :unquote },
28
- 'text-decoration' => { key: :styles, set: :append_symbol },
27
+ 'text-decoration' => { key: :styles, set: :append_text_decoration },
28
+ 'vertical-align' => { key: :styles, set: :append_styles },
29
+ 'white-space' => { key: :white_space, set: :convert_symbol },
29
30
  # tag opening styles
31
+ 'break-before' => { key: :break_before, set: :convert_symbol },
30
32
  'margin-top' => { key: :margin_top, set: :convert_size },
31
33
  'padding-top' => { key: :padding_top, set: :convert_size },
32
34
  # tag closing styles
35
+ 'break-after' => { key: :break_after, set: :convert_symbol },
33
36
  'margin-bottom' => { key: :margin_bottom, set: :convert_size },
34
37
  'padding-bottom' => { key: :padding_bottom, set: :convert_size },
35
38
  # block styles
36
- 'left' => { key: :left, set: :convert_size },
39
+ 'bottom' => { key: :bottom, set: :convert_size, options: :height },
40
+ 'left' => { key: :left, set: :convert_size, options: :width },
37
41
  'line-height' => { key: :leading, set: :convert_size },
38
42
  'margin-left' => { key: :margin_left, set: :convert_size },
39
43
  'padding-left' => { key: :padding_left, set: :convert_size },
40
44
  'position' => { key: :position, set: :convert_symbol },
45
+ 'right' => { key: :right, set: :convert_size, options: :width },
41
46
  'text-align' => { key: :align, set: :convert_symbol },
42
- 'top' => { key: :top, set: :convert_size }
47
+ 'top' => { key: :top, set: :convert_size, options: :height }
43
48
  }.freeze
44
49
 
45
50
  STYLES_MERGE = %i[margin_left padding_left].freeze
@@ -48,10 +53,6 @@ module PrawnHtml
48
53
  def initialize(attributes = {})
49
54
  super
50
55
  @styles = {} # result styles
51
- return unless style
52
-
53
- styles_hash = Attributes.parse_styles(style)
54
- process_styles(styles_hash)
55
56
  end
56
57
 
57
58
  # Processes the data attributes
@@ -64,21 +65,13 @@ module PrawnHtml
64
65
  end
65
66
  end
66
67
 
67
- # Merge already parsed styles
68
+ # Merge text styles
68
69
  #
69
- # @param parsed_styles [Hash] hash of parsed styles
70
- def merge_styles!(parsed_styles)
71
- @styles.merge!(parsed_styles)
72
- end
73
-
74
- # Processes the styles attributes
75
- #
76
- # @param styles_hash [Hash] hash of styles attributes
77
- def process_styles(styles_hash)
78
- styles_hash.each do |key, value|
79
- apply_rule!(@styles, STYLES_LIST[key], value)
80
- end
81
- @styles
70
+ # @param text_styles [String] styles to parse and process
71
+ # @param options [Hash] options (container width/height/etc.)
72
+ def merge_text_styles!(text_styles, options: {})
73
+ hash_styles = Attributes.parse_styles(text_styles)
74
+ process_styles(hash_styles, options: options) unless hash_styles.empty?
82
75
  end
83
76
 
84
77
  class << self
@@ -109,13 +102,32 @@ module PrawnHtml
109
102
 
110
103
  private
111
104
 
112
- def apply_rule!(result, rule, value)
105
+ def process_styles(hash_styles, options:)
106
+ hash_styles.each do |key, value|
107
+ rule = evaluate_rule(key, value)
108
+ apply_rule!(merged_styles: @styles, rule: rule, value: value, options: options)
109
+ end
110
+ @styles
111
+ end
112
+
113
+ def evaluate_rule(rule_key, attr_value)
114
+ rule = STYLES_LIST[rule_key]
115
+ if rule && rule[:set] == :append_text_decoration
116
+ return { key: :callback, set: :callback_strike_through } if attr_value == 'line-through'
117
+
118
+ return { key: :styles, set: :append_styles }
119
+ end
120
+ rule
121
+ end
122
+
123
+ def apply_rule!(merged_styles:, rule:, value:, options:)
113
124
  return unless rule
114
125
 
115
- if rule[:set] == :append_symbol
116
- (result[rule[:key]] ||= []) << Utils.convert_symbol(value)
126
+ if rule[:set] == :append_styles
127
+ (merged_styles[rule[:key]] ||= []) << Utils.normalize_style(value)
117
128
  else
118
- result[rule[:key]] = Utils.send(rule[:set], value)
129
+ opts = rule[:options] ? options[rule[:options]] : nil
130
+ merged_styles[rule[:key]] = Utils.send(rule[:set], value, options: opts)
119
131
  end
120
132
  end
121
133
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PrawnHtml
4
+ module Callbacks
5
+ class Background
6
+ DEF_HIGHLIGHT = 'ffff00'
7
+
8
+ def initialize(pdf, color = nil)
9
+ @pdf = pdf
10
+ @color = color || DEF_HIGHLIGHT
11
+ end
12
+
13
+ def render_behind(fragment)
14
+ top, left = fragment.top_left
15
+ @pdf.draw_rectangle(x: left, y: top, width: fragment.width, height: fragment.height, color: @color)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -8,10 +8,10 @@ module PrawnHtml
8
8
  end
9
9
 
10
10
  def render_in_front(fragment)
11
- y = (fragment.top_left[1] + fragment.bottom_left[1]) / 2
12
- @pdf.stroke do
13
- @pdf.line [fragment.top_left[0], y], [fragment.top_right[0], y]
14
- end
11
+ x1 = fragment.left
12
+ x2 = fragment.right
13
+ y = (fragment.top + fragment.bottom) / 2
14
+ @pdf.underline(x1: x1, x2: x2, y: y)
15
15
  end
16
16
  end
17
17
  end
@@ -2,15 +2,17 @@
2
2
 
3
3
  module PrawnHtml
4
4
  class Context < Array
5
- DEF_FONT_SIZE = 10.3
5
+ DEF_FONT_SIZE = 16 * PX
6
6
 
7
- attr_accessor :last_margin, :last_text_node
7
+ attr_reader :previous_tag
8
+ attr_accessor :last_text_node
8
9
 
9
10
  # Init the Context
10
11
  def initialize(*_args)
11
12
  super
12
- @last_margin = 0
13
13
  @last_text_node = false
14
+ @merged_styles = nil
15
+ @previous_tag = nil
14
16
  end
15
17
 
16
18
  # Add an element to the context
@@ -25,6 +27,7 @@ module PrawnHtml
25
27
  element.parent = last
26
28
  push(element)
27
29
  element.on_context_add(self) if element.respond_to?(:on_context_add)
30
+ @merged_styles = nil
28
31
  self
29
32
  end
30
33
 
@@ -32,9 +35,7 @@ module PrawnHtml
32
35
  #
33
36
  # @return [String] before content string
34
37
  def before_content
35
- return '' if empty? || !last.respond_to?(:tag_styles)
36
-
37
- last.tag_styles[:before_content].to_s
38
+ (last.respond_to?(:before_content) && last.before_content) || ''
38
39
  end
39
40
 
40
41
  # Merges the context block styles
@@ -51,11 +52,21 @@ module PrawnHtml
51
52
  # Merge the context styles for text nodes
52
53
  #
53
54
  # @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
55
+ def merged_styles
56
+ @merged_styles ||=
57
+ each_with_object(base_styles) do |element, res|
58
+ evaluate_element_styles(element, res)
59
+ element.update_styles(res) if element.respond_to?(:update_styles)
60
+ end
61
+ end
62
+
63
+ # Remove the last element from the context
64
+ def remove_last
65
+ last.on_context_remove(self) if last.respond_to?(:on_context_remove)
66
+ @merged_styles = nil
67
+ @last_text_node = false
68
+ @previous_tag = last.tag
69
+ pop
59
70
  end
60
71
 
61
72
  private
@@ -7,47 +7,37 @@ module PrawnHtml
7
7
 
8
8
  # Init the DocumentRenderer
9
9
  #
10
- # @param pdf [Prawn::Document] target Prawn PDF document
10
+ # @param pdf [PdfWrapper] target PDF wrapper
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,41 +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
- move_down = context.last_margin + tag_styles[:padding_bottom].to_f
107
- pdf.move_down(move_down) if move_down > 0
94
+ @last_margin = tag_styles[:margin_bottom].to_f
95
+ pdf.advance_cursor(last_margin + tag_styles[:padding_bottom].to_f)
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
113
- pdf.move_down(move_down) if move_down > 0
101
+ move_down = (tag_styles[:margin_top].to_f - last_margin) + tag_styles[:padding_top].to_f
102
+ pdf.advance_cursor(move_down) if move_down > 0
103
+ pdf.start_new_page if tag_styles[:break_before]
104
+ end
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
114
111
  end
115
112
 
116
113
  def output_content(buffer, block_styles)
117
- buffer.each { |item| item[:callback] = item[:callback].new(pdf, item) if item[:callback] }
114
+ apply_callbacks(buffer)
118
115
  left_indent = block_styles[:margin_left].to_f + block_styles[:padding_left].to_f
119
- options = block_styles.slice(:align, :leading, :mode, :padding_left)
120
- options[:indent_paragraphs] = left_indent if left_indent > 0
121
- formatted_text(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)
122
119
  end
123
120
 
124
- def formatted_text(buffer, options, bounding_box: nil)
125
- return pdf.formatted_text(buffer, options) unless bounding_box
126
-
127
- current_y = pdf.cursor
128
- pdf.bounding_box(*bounding_box) do
129
- pdf.formatted_text(buffer, options)
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)
130
126
  end
131
- pdf.move_cursor_to(current_y)
132
127
  end
133
128
 
134
- def bounds(block_styles)
129
+ def adjust_leading(buffer, leading)
130
+ return leading if leading
131
+
132
+ (buffer.map { |item| item[:size] || Context::DEF_FONT_SIZE }.max * 0.055).round(4)
133
+ end
134
+
135
+ def bounds(buffer, options, block_styles)
135
136
  return unless block_styles[:position] == :absolute
136
137
 
137
- y = pdf.bounds.height - (block_styles[:top] || 0)
138
- w = pdf.bounds.width - (block_styles[:left] || 0)
139
- [[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 }]
140
151
  end
141
152
  end
142
153
  end