prawn-html 0.5.0 → 0.6.4

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: e82e0a464e39fd4c47c399cb043ceefe090524b29ead1c06795a484d4c648388
4
- data.tar.gz: e124a7fb9800a434f42d403b478c6a6cb30b4e7d1e7e540d015028dec6daaa59
3
+ metadata.gz: c773502449714d4f0a6f2401721ba67a349731ef9f0c525216864b355fbe76c8
4
+ data.tar.gz: 8857a7761f343367a4c6496757c767b6b62b46d0d840df96a7d545c3693c744f
5
5
  SHA512:
6
- metadata.gz: c08e1252e2c9c8f1591840179549d3e14483d91361b1a9f56d96f23cfa30829b98d8d2ace33808a73a9467e1ff37054d7f462d1c7170db273a4ff83b203d807f
7
- data.tar.gz: '0979bd9a66e463ee8e4ea3c141a2d4e2fcc0e3b1bc82397aeda2e357c10c4518b00b1e1e4fc3f2ec786070c6d229241423cd5933a89fbc9137bdccc211d22a44'
6
+ metadata.gz: 51be5bfbbf4427f6124a4a00cfb399017d6b806d8d3d2bb228fc81b7547a3cbaffe535767c5270a2666bed0a9abd89099d156dd79b1ea638d39101e528207be0
7
+ data.tar.gz: 520221561fc067919e098349fe6fab940290f6de419b37722679eacbb959055700620539c067d6a57786e7d4396bd0a4659e0f95428b09a96c3a93c55e174bda
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Prawn HTML
2
2
  [![gem version](https://badge.fury.io/rb/prawn-html.svg)](https://rubygems.org/gems/prawn-html)
3
+ [![gem downloads](https://badgen.net/rubygems/dt/prawn-html)](https://rubygems.org/gems/prawn-html)
4
+ [![maintainability](https://api.codeclimate.com/v1/badges/db674db00817d56ca1e9/maintainability)](https://codeclimate.com/github/blocknotes/prawn-html/maintainability)
3
5
  [![linters](https://github.com/blocknotes/prawn-html/actions/workflows/linters.yml/badge.svg)](https://github.com/blocknotes/prawn-html/actions/workflows/linters.yml)
4
6
  [![specs](https://github.com/blocknotes/prawn-html/actions/workflows/specs.yml/badge.svg)](https://github.com/blocknotes/prawn-html/actions/workflows/specs.yml)
5
7
 
@@ -35,34 +37,34 @@ To check some examples with the PDF output see [examples](examples/) folder.
35
37
 
36
38
  ## Supported tags & attributes
37
39
 
38
- HTML tags:
39
-
40
- - **a**: link
41
- - **b**: bold
42
- - **blockquote**: block quotation element
43
- - **br**: new line
44
- - **code**: inline code element
45
- - **del**: strike-through
46
- - **div**: block element
47
- - **em**: italic
48
- - **h1** - **h6**: headings
49
- - **hr**: horizontal line
50
- - **i**: italic
51
- - **ins**: underline
52
- - **img**: image
53
- - **li**: list item
54
- - **mark**: highlight
55
- - **ol**: ordered list
56
- - **p**: block element
57
- - **pre**: preformatted text element
58
- - **s**: strike-through
59
- - **small**: smaller text
60
- - **span**: inline element
61
- - **strong**: bold
62
- - **sub**: subscript element
63
- - **sup**: superscript element
64
- - **u**: underline
65
- - **ul**: unordered list
40
+ HTML tags (using MDN definitions):
41
+
42
+ - **a**: the Anchor element
43
+ - **b**: the Bring Attention To element
44
+ - **blockquote**: the Block Quotation element
45
+ - **br**: the Line Break element
46
+ - **code**: the Inline Code element
47
+ - **del**: the Deleted Text element
48
+ - **div**: the Content Division element
49
+ - **em**: the Emphasis element
50
+ - **h1** - **h6**: the HTML Section Heading elements
51
+ - **hr**: the Thematic Break (Horizontal Rule) element
52
+ - **i**: the Idiomatic Text element
53
+ - **ins**: the added text element
54
+ - **img**: the Image Embed element
55
+ - **li**: the list item element
56
+ - **mark**: the Mark Text element
57
+ - **ol**: the Ordered List element
58
+ - **p**: the Paragraph element
59
+ - **pre**: the Preformatted Text element
60
+ - **s**: the strike-through text element
61
+ - **small**: the side comment element
62
+ - **span**: the generic inline element
63
+ - **strong**: the Strong Importance element
64
+ - **sub**: the Subscript element
65
+ - **sup**: the Superscript element
66
+ - **u**: the Unarticulated Annotation (Underline) element
67
+ - **ul**: the Unordered List element
66
68
 
67
69
  CSS attributes (dimensional units are ignored and considered in pixel):
68
70
 
@@ -90,6 +92,8 @@ CSS attributes (dimensional units are ignored and considered in pixel):
90
92
  - **top**: see *position (absolute)*
91
93
  - **width**: for *img* tag, support also percentage, ex. `<img src="image.jpg" style="width: 50%; height: 200px"/>`
92
94
 
95
+ The above attributes supports the `initial` value to reset them to their original value.
96
+
93
97
  For colors, the supported formats are:
94
98
  - 3 hex digits, ex. `color: #FB1`;
95
99
  - 6 hex digits, ex. `color: #abcdef`;
data/lib/prawn-html.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PrawnHtml
4
+ ADJUST_LEADING = { nil => 0.18, 'Courier' => -0.07, 'Helvetica' => -0.17, 'Times-Roman' => 0.03 }.freeze
4
5
  PX = 0.6 # conversion constant for pixel sixes
5
6
 
6
7
  COLORS = {
@@ -1,10 +1,11 @@
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
11
  block: %i[align bottom leading left margin_left padding_left position right top],
@@ -19,13 +20,13 @@ module PrawnHtml
19
20
  'color' => { key: :color, set: :convert_color },
20
21
  'font-family' => { key: :font, set: :unquote },
21
22
  'font-size' => { key: :size, set: :convert_size },
22
- 'font-style' => { key: :styles, set: :append_styles },
23
- '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] },
24
25
  'href' => { key: :link, set: :copy_value },
25
26
  'letter-spacing' => { key: :character_spacing, set: :convert_float },
26
27
  'list-style-type' => { key: :list_style_type, set: :unquote },
27
- 'text-decoration' => { key: :styles, set: :append_text_decoration },
28
- 'vertical-align' => { 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] },
29
30
  'white-space' => { key: :white_space, set: :convert_symbol },
30
31
  # tag opening styles
31
32
  'break-before' => { key: :break_before, set: :convert_symbol },
@@ -44,7 +45,9 @@ module PrawnHtml
44
45
  'position' => { key: :position, set: :convert_symbol },
45
46
  'right' => { key: :right, set: :convert_size, options: :width },
46
47
  'text-align' => { key: :align, set: :convert_symbol },
47
- 'top' => { key: :top, set: :convert_size, options: :height }
48
+ 'top' => { key: :top, set: :convert_size, options: :height },
49
+ # special styles
50
+ 'text-decoration-line-through' => { key: :callback, set: :callback_strike_through }
48
51
  }.freeze
49
52
 
50
53
  STYLES_MERGE = %i[margin_left padding_left].freeze
@@ -53,6 +56,7 @@ module PrawnHtml
53
56
  def initialize(attributes = {})
54
57
  super
55
58
  @styles = {} # result styles
59
+ @initial = Set.new
56
60
  end
57
61
 
58
62
  # Processes the data attributes
@@ -74,6 +78,33 @@ module PrawnHtml
74
78
  process_styles(hash_styles, options: options) unless hash_styles.empty?
75
79
  end
76
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
106
+ end
107
+
77
108
  class << self
78
109
  # Merges attributes
79
110
  #
@@ -105,29 +136,30 @@ module PrawnHtml
105
136
  def process_styles(hash_styles, options:)
106
137
  hash_styles.each do |key, value|
107
138
  rule = evaluate_rule(key, value)
139
+ next unless rule
140
+
108
141
  apply_rule!(merged_styles: @styles, rule: rule, value: value, options: options)
109
142
  end
110
143
  @styles
111
144
  end
112
145
 
113
146
  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
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]
121
151
  end
122
152
 
123
153
  def apply_rule!(merged_styles:, rule:, value:, options:)
124
- return unless rule
154
+ return (@initial << rule) if value == 'initial'
125
155
 
126
156
  if rule[:set] == :append_styles
127
- (merged_styles[rule[:key]] ||= []) << Utils.normalize_style(value)
157
+ val = Utils.normalize_style(value, rule[:values])
158
+ (merged_styles[rule[:key]] ||= []) << val if val
128
159
  else
129
160
  opts = rule[:options] ? options[rule[:options]] : nil
130
- merged_styles[rule[:key]] = Utils.send(rule[:set], value, options: opts)
161
+ val = Utils.send(rule[:set], value, options: opts)
162
+ merged_styles[rule[:key]] = val if val
131
163
  end
132
164
  end
133
165
  end
@@ -2,7 +2,9 @@
2
2
 
3
3
  module PrawnHtml
4
4
  class Context < Array
5
- DEF_FONT_SIZE = 16 * PX
5
+ DEFAULT_STYLES = {
6
+ size: 16 * PX
7
+ }.freeze
6
8
 
7
9
  attr_reader :previous_tag
8
10
  attr_accessor :last_text_node
@@ -54,9 +56,9 @@ module PrawnHtml
54
56
  # @return [Hash] the hash of merged styles
55
57
  def merged_styles
56
58
  @merged_styles ||=
57
- each_with_object(base_styles) do |element, res|
59
+ each_with_object(DEFAULT_STYLES.dup) do |element, res|
58
60
  evaluate_element_styles(element, res)
59
- element.update_styles(res) if element.respond_to?(:update_styles)
61
+ element.update_styles(res)
60
62
  end
61
63
  end
62
64
 
@@ -65,18 +67,19 @@ module PrawnHtml
65
67
  last.on_context_remove(self) if last.respond_to?(:on_context_remove)
66
68
  @merged_styles = nil
67
69
  @last_text_node = false
68
- @previous_tag = last.tag
70
+ @previous_tag = last
69
71
  pop
70
72
  end
71
73
 
72
- private
73
-
74
- def base_styles
75
- {
76
- size: DEF_FONT_SIZE
77
- }
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
78
79
  end
79
80
 
81
+ private
82
+
80
83
  def evaluate_element_styles(element, res)
81
84
  styles = element.styles.slice(*Attributes::STYLES_APPLY[:text_node])
82
85
  styles.each do |key, val|
@@ -12,6 +12,8 @@ module PrawnHtml
12
12
  @buffer = []
13
13
  @context = Context.new
14
14
  @last_margin = 0
15
+ @last_text = ''
16
+ @last_tag_open = false
15
17
  @pdf = pdf
16
18
  end
17
19
 
@@ -22,6 +24,8 @@ module PrawnHtml
22
24
  render_if_needed(element)
23
25
  apply_tag_close_styles(element)
24
26
  context.remove_last
27
+ @last_tag_open = false
28
+ @last_text = ''
25
29
  end
26
30
 
27
31
  # On tag open callback
@@ -38,6 +42,7 @@ module PrawnHtml
38
42
  options = { width: pdf.page_width, height: pdf.page_height }
39
43
  tag_class.new(tag_name, attributes: attributes, options: options).tap do |element|
40
44
  setup_element(element, element_styles: element_styles)
45
+ @last_tag_open = true
41
46
  end
42
47
  end
43
48
 
@@ -47,9 +52,10 @@ module PrawnHtml
47
52
  #
48
53
  # @return [NilClass] nil value (=> no element)
49
54
  def on_text_node(content)
50
- return if content.match?(/\A\s*\Z/)
55
+ return if context.previous_tag&.block? && content.match?(/\A\s*\Z/)
51
56
 
52
- buffer << context.merged_styles.merge(text: prepare_text(content))
57
+ text = prepare_text(content)
58
+ buffer << context.merged_styles.merge(text: text) unless text.empty?
53
59
  context.last_text_node = true
54
60
  nil
55
61
  end
@@ -70,17 +76,13 @@ module PrawnHtml
70
76
  attr_reader :buffer, :context, :last_margin, :pdf
71
77
 
72
78
  def setup_element(element, element_styles:)
73
- add_space_if_needed unless render_if_needed(element)
79
+ render_if_needed(element)
74
80
  context.add(element)
75
81
  element.process_styles(element_styles: element_styles)
76
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
@@ -104,10 +106,13 @@ module PrawnHtml
104
106
  end
105
107
 
106
108
  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
109
+ before_content = context.before_content
110
+ text = before_content ? ::Oga::HTML::Entities.decode(before_content) : ''
111
+ return (@last_text = text + content) if context.white_space_pre?
112
+
113
+ content = content.lstrip if @last_text[-1] == ' ' || @last_tag_open
114
+ text += content.tr("\n", ' ').squeeze(' ')
115
+ @last_text = text
111
116
  end
112
117
 
113
118
  def output_content(buffer, block_styles)
@@ -129,7 +134,10 @@ module PrawnHtml
129
134
  def adjust_leading(buffer, leading)
130
135
  return leading if leading
131
136
 
132
- (buffer.map { |item| item[:size] || Context::DEF_FONT_SIZE }.max * 0.055).round(4)
137
+ leadings = buffer.map do |item|
138
+ (item[:size] || Context::DEFAULT_STYLES[:size]) * (ADJUST_LEADING[item[:font]] || ADJUST_LEADING[nil])
139
+ end
140
+ leadings.max.round(4)
133
141
  end
134
142
 
135
143
  def bounds(buffer, options, block_styles)
@@ -2,12 +2,17 @@
2
2
 
3
3
  module PrawnHtml
4
4
  class Tag
5
+ extend Forwardable
6
+
5
7
  CALLBACKS = {
6
8
  'Background' => Callbacks::Background,
7
9
  'StrikeThrough' => Callbacks::StrikeThrough
8
10
  }.freeze
11
+
9
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
10
13
 
14
+ def_delegators :@attrs, :styles, :update_styles
15
+
11
16
  attr_accessor :parent
12
17
  attr_reader :attrs, :tag
13
18
 
@@ -45,6 +50,7 @@ module PrawnHtml
45
50
  attrs.merge_text_styles!(tag_styles, options: options) if respond_to?(:tag_styles)
46
51
  attrs.merge_text_styles!(element_styles, options: options) if element_styles
47
52
  attrs.merge_text_styles!(attrs.style, options: options)
53
+ attrs.merge_text_styles!(extra_styles, options: options) if respond_to?(:extra_styles)
48
54
  end
49
55
 
50
56
  # Styles to apply on tag closing
@@ -54,13 +60,6 @@ module PrawnHtml
54
60
  styles.slice(*Attributes::STYLES_APPLY[:tag_close])
55
61
  end
56
62
 
57
- # Styles hash
58
- #
59
- # @return [Hash] hash of styles
60
- def styles
61
- attrs.styles
62
- end
63
-
64
63
  # Styles to apply on tag opening
65
64
  #
66
65
  # @return [Hash] hash of styles to apply
@@ -5,12 +5,13 @@ module PrawnHtml
5
5
  class A < Tag
6
6
  ELEMENTS = [:a].freeze
7
7
 
8
- def tag_styles
9
- return unless attrs.href
8
+ def extra_styles
9
+ attrs.href ? "href: #{attrs.href}" : nil
10
+ end
10
11
 
12
+ def tag_styles
11
13
  <<~STYLES
12
14
  color: #00E;
13
- href: #{attrs.href};
14
15
  text-decoration: underline;
15
16
  STYLES
16
17
  end
@@ -12,7 +12,7 @@ module PrawnHtml
12
12
  end
13
13
 
14
14
  def custom_render(pdf, context)
15
- return if context.last_text_node || context.previous_tag != :br
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
@@ -13,7 +13,9 @@ module PrawnHtml
13
13
  end
14
14
 
15
15
  def before_content
16
- @counter ? "#{@counter}. " : "#{@symbol} "
16
+ return if @before_content_once
17
+
18
+ @before_content_once = @counter ? "#{@counter}. " : "#{@symbol} "
17
19
  end
18
20
 
19
21
  def block_styles
@@ -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
@@ -45,7 +45,7 @@ module PrawnHtml
45
45
 
46
46
  if val.match /\A#([a-f0-9]{3})\Z/ # rubocop:disable Performance/RedundantMatch
47
47
  r, g, b = Regexp.last_match[1].chars
48
- return r * 2 + g * 2 + b * 2
48
+ return (r * 2) + (g * 2) + (b * 2)
49
49
  end
50
50
  if val.match /\Argb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\Z/ # rubocop:disable Performance/RedundantMatch
51
51
  r, g, b = Regexp.last_match[1..3].map { |v| v.to_i.to_s(16) }
@@ -103,11 +103,13 @@ module PrawnHtml
103
103
  # Normalize a style value
104
104
  #
105
105
  # @param value [String] string value
106
+ # @param accepted_values [Array] allowlist of valid values (symbols)
106
107
  #
107
108
  # @return [Symbol] style value or nil
108
- def normalize_style(value)
109
+ def normalize_style(value, accepted_values)
109
110
  val = value&.strip&.downcase
110
- NORMALIZE_STYLES[val]
111
+ ret = NORMALIZE_STYLES[val]
112
+ accepted_values.include?(ret) ? ret : nil
111
113
  end
112
114
 
113
115
  # Unquotes a string
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PrawnHtml # :nodoc:
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.4'
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.5.0
4
+ version: 0.6.4
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-09-09 00:00:00.000000000 Z
11
+ date: 2022-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oga
@@ -83,7 +83,10 @@ files:
83
83
  homepage: https://github.com/blocknotes/prawn-html
84
84
  licenses:
85
85
  - MIT
86
- metadata: {}
86
+ metadata:
87
+ homepage_uri: https://github.com/blocknotes/prawn-html
88
+ source_code_uri: https://github.com/blocknotes/prawn-html
89
+ rubygems_mfa_required: 'true'
87
90
  post_install_message:
88
91
  rdoc_options: []
89
92
  require_paths:
@@ -99,7 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
99
102
  - !ruby/object:Gem::Version
100
103
  version: '0'
101
104
  requirements: []
102
- rubygems_version: 3.1.4
105
+ rubygems_version: 3.1.6
103
106
  signing_key:
104
107
  specification_version: 4
105
108
  summary: Prawn PDF - HTML renderer