prawn-html 0.5.0 → 0.6.4

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