prawn-html 0.3.0 → 0.5.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: 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