prawn-markup 0.1.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.
@@ -0,0 +1,97 @@
1
+ module Prawn
2
+ module Markup
3
+ module Processor::Tables
4
+ def self.prepended(base)
5
+ base.known_elements.push('table', 'tr', 'td', 'th')
6
+ end
7
+
8
+ def start_table
9
+ if current_table
10
+ add_cell_text_node(current_cell)
11
+ else
12
+ add_current_text
13
+ end
14
+ table_stack.push([])
15
+ end
16
+
17
+ def end_table
18
+ data = table_stack.pop
19
+ return if data.empty? || data.all?(&:empty?)
20
+
21
+ if table_stack.empty?
22
+ add_table(data)
23
+ else
24
+ current_cell.nodes << data
25
+ end
26
+ end
27
+
28
+ def start_tr
29
+ current_table << []
30
+ end
31
+
32
+ def start_td
33
+ current_table.last << Elements::Cell.new(width: style_properties['width'])
34
+ end
35
+
36
+ def start_th
37
+ current_table.last << Elements::Cell.new(width: style_properties['width'], header: true)
38
+ end
39
+
40
+ def end_td
41
+ add_cell_text_node(current_cell)
42
+ end
43
+ alias end_th end_td
44
+
45
+ def start_img
46
+ if current_table
47
+ add_cell_image(current_cell)
48
+ else
49
+ super
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :table_stack
56
+
57
+ def reset
58
+ @table_stack = []
59
+ super
60
+ end
61
+
62
+ def current_table
63
+ table_stack.last
64
+ end
65
+
66
+ def current_cell
67
+ current_table.last.last
68
+ end
69
+
70
+ def inside_container?
71
+ super || current_table
72
+ end
73
+
74
+ def add_cell_text_node(cell, options = {})
75
+ return unless buffered_text?
76
+ cell.nodes << options.merge(content: dump_text.strip)
77
+ end
78
+
79
+ def add_cell_image(cell)
80
+ add_cell_text_node(cell)
81
+ img = image_properties(current_attrs['src'])
82
+ cell.nodes << img || invalid_image_placeholder
83
+ end
84
+
85
+ def add_table(cells)
86
+ Builders::TableBuilder.new(pdf, cells, pdf.bounds.width, options).draw
87
+ put_bottom_margin(text_margin_bottom)
88
+ rescue Prawn::Errors::CannotFit => e
89
+ append_text(table_too_large_placeholder(e))
90
+ end
91
+
92
+ def table_too_large_placeholder(error)
93
+ placeholder_value(%i[table placeholder too_large], error) || '[table content too large]'
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,176 @@
1
+ module Prawn
2
+ module Markup
3
+ module Processor::Text
4
+ def self.prepended(base)
5
+ base.known_elements.push('p', 'br', 'div', 'b', 'strong', 'i', 'em', 'u', 'a', 'hr')
6
+ end
7
+
8
+ def start_br
9
+ append_text("\n")
10
+ end
11
+
12
+ def start_p
13
+ handle_text_element
14
+ end
15
+
16
+ def end_p
17
+ if inside_container?
18
+ append_new_line
19
+ append_text("\n")
20
+ else
21
+ add_paragraph
22
+ end
23
+ end
24
+
25
+ def start_div
26
+ handle_text_element
27
+ end
28
+
29
+ def end_div
30
+ handle_text_element
31
+ end
32
+
33
+ def start_a
34
+ append_text("<link href=\"#{current_attrs['href']}\">")
35
+ end
36
+
37
+ def end_a
38
+ append_text('</link>')
39
+ end
40
+
41
+ def start_b
42
+ append_text('<b>')
43
+ end
44
+ alias start_strong start_b
45
+
46
+ def end_b
47
+ append_text('</b>')
48
+ end
49
+ alias end_strong end_b
50
+
51
+ def start_i
52
+ append_text('<i>')
53
+ end
54
+ alias start_em start_i
55
+
56
+ def end_i
57
+ append_text('</i>')
58
+ end
59
+ alias end_em end_i
60
+
61
+ def start_hr
62
+ return if inside_container?
63
+
64
+ put_bottom_margin(nil)
65
+ add_current_text
66
+ pdf.move_down(hr_vertical_margin_top)
67
+ pdf.stroke_horizontal_rule
68
+ pdf.move_down(hr_vertical_margin_bottom)
69
+ end
70
+
71
+ def end_document
72
+ add_current_text
73
+ end
74
+
75
+ private
76
+
77
+ def handle_text_element
78
+ if inside_container?
79
+ append_new_line
80
+ else
81
+ add_current_text
82
+ end
83
+ end
84
+
85
+ def append_new_line
86
+ append_text("\n") if buffered_text? && text_buffer[-1] != "\n"
87
+ end
88
+
89
+ def add_paragraph
90
+ text = dump_text
91
+ text.gsub!(/[^\n]/, '') if text.strip.empty?
92
+ unless text.empty?
93
+ add_bottom_margin
94
+ add_formatted_text(text, text_options)
95
+ put_bottom_margin(text_margin_bottom)
96
+ end
97
+ end
98
+
99
+ def add_current_text(options = text_options)
100
+ add_bottom_margin
101
+ return unless buffered_text?
102
+
103
+ string = dump_text
104
+ string.strip!
105
+ add_formatted_text(string, options)
106
+ end
107
+
108
+ def add_bottom_margin
109
+ if @bottom_margin
110
+ pdf.move_down(@bottom_margin)
111
+ @bottom_margin = nil
112
+ end
113
+ end
114
+
115
+ def add_formatted_text(string, options = text_options)
116
+ with_font(options) do
117
+ pdf.text(string, options)
118
+ end
119
+ end
120
+
121
+ def with_font(options)
122
+ pdf.font(options[:font] || pdf.font.family,
123
+ size: options[:size],
124
+ style: options[:style]) do
125
+ return yield
126
+ end
127
+ end
128
+
129
+ def hr_vertical_margin_top
130
+ @hr_vertical_margin_top ||=
131
+ (text_options[:size] || pdf.font_size) / 2.0
132
+ end
133
+
134
+ def hr_vertical_margin_bottom
135
+ @hr_vertical_margin_bottom ||= with_font(text_options) do
136
+ hr_vertical_margin_top +
137
+ pdf.font.descender +
138
+ text_leading -
139
+ pdf.line_width
140
+ end
141
+ end
142
+
143
+ def reset
144
+ super
145
+ text_margin_bottom # pre-calculate
146
+ end
147
+
148
+ def text_margin_bottom
149
+ options[:text] ||= {}
150
+ options[:text][:margin_bottom] ||= default_text_margin_bottom
151
+ end
152
+
153
+ def default_text_margin_bottom
154
+ with_font(text_options) do
155
+ pdf.font.line_gap +
156
+ pdf.font.descender +
157
+ text_leading
158
+ end
159
+ end
160
+
161
+ def text_leading
162
+ text_options[:leading] || pdf.default_leading
163
+ end
164
+
165
+ def text_options
166
+ @text_options ||= HashMerger.deep(default_text_options, options[:text] || {})
167
+ end
168
+
169
+ def default_text_options
170
+ {
171
+ inline_format: true
172
+ }
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,19 @@
1
+ module Prawn
2
+ module Markup
3
+ module HashMerger
4
+ def self.deep(hash, other)
5
+ hash.merge(other) do |_key, this_val, other_val|
6
+ if this_val.is_a?(Hash) && other_val.is_a?(Hash)
7
+ deep(this_val, other_val)
8
+ else
9
+ other_val
10
+ end
11
+ end
12
+ end
13
+
14
+ def self.enhance(options, key, hash)
15
+ options[key] = hash.merge(options[key])
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,48 @@
1
+ module Prawn
2
+ module Markup
3
+ # Normalizes HTML markup:
4
+ # * assert that self-closing tags are always closed
5
+ # * replace html entities with their UTF-8 correspondent string
6
+ # * normalize white space
7
+ # * wrap entire content into <root> tag
8
+ class Normalizer
9
+ SELF_CLOSING_ELEMENTS = %w[br img hr].freeze
10
+
11
+ REPLACE_ENTITIES = {
12
+ nbsp: ' '
13
+ }.freeze
14
+
15
+ attr_reader :html
16
+
17
+ def initialize(html)
18
+ @html = html.dup
19
+ end
20
+
21
+ def normalize
22
+ close_self_closing_elements
23
+ normalize_spaces
24
+ replace_html_entities
25
+ "<root>#{html}</root>"
26
+ end
27
+
28
+ private
29
+
30
+ def close_self_closing_elements
31
+ html.gsub!(/<(#{SELF_CLOSING_ELEMENTS.join('|')})[^>]*>/i) do |tag|
32
+ tag[-1] = '/>' unless tag.end_with?('/>')
33
+ tag
34
+ end
35
+ end
36
+
37
+ def normalize_spaces
38
+ html.gsub!(/\s+/, ' ')
39
+ end
40
+
41
+ def replace_html_entities
42
+ REPLACE_ENTITIES.each do |entity, string|
43
+ html.gsub!(/&#{entity};/, string)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,31 @@
1
+ module Prawn
2
+ module Markup
3
+ class SizeConverter
4
+ attr_reader :max
5
+
6
+ def initialize(max)
7
+ @max = max
8
+ end
9
+
10
+ def parse(width)
11
+ return nil if width.to_s.strip.empty? || width.to_s == 'auto'
12
+
13
+ points = convert(width)
14
+ max ? [points, max].min : points
15
+ end
16
+
17
+ def convert(string)
18
+ value = string.to_f
19
+ if string.end_with?('%')
20
+ value * max / 100.0
21
+ elsif string.end_with?('cm')
22
+ value.cm
23
+ elsif string.end_with?('mm')
24
+ value.mm
25
+ else
26
+ value
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ module Prawn
2
+ module Markup
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,37 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'prawn/markup/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'prawn-markup'
7
+ spec.version = Prawn::Markup::VERSION
8
+ spec.authors = ['Pascal Zumkehr']
9
+ spec.email = ['zumkehr@puzzle.ch']
10
+
11
+ spec.summary = 'Parse simple HTML markup to include in Prawn PDFs'
12
+ spec.description = 'Adds simple HTML snippets into Prawn-generated PDFs. ' \
13
+ 'All elements are layouted vertically using Prawn\'s formatting ' \
14
+ 'options. A major use case for this gem is to include ' \
15
+ 'WYSIWYG-generated HTML parts into server-generated PDF documents.'
16
+ spec.homepage = 'https://github.com/puzzle/prawn-markup'
17
+ spec.license = 'MIT'
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features)/})
21
+ end
22
+ spec.bindir = 'exe'
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ['lib']
25
+
26
+ spec.add_dependency 'nokogiri'
27
+ spec.add_dependency 'prawn'
28
+ spec.add_dependency 'prawn-table'
29
+
30
+ spec.add_development_dependency 'bundler'
31
+ spec.add_development_dependency 'byebug'
32
+ spec.add_development_dependency 'pdf-inspector'
33
+ spec.add_development_dependency 'rake'
34
+ spec.add_development_dependency 'rspec'
35
+ spec.add_development_dependency 'rubocop'
36
+ spec.add_development_dependency 'simplecov'
37
+ end
metadata ADDED
@@ -0,0 +1,217 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prawn-markup
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pascal Zumkehr
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-09-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: prawn
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: prawn-table
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pdf-inspector
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: simplecov
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: Adds simple HTML snippets into Prawn-generated PDFs. All elements are
154
+ layouted vertically using Prawn's formatting options. A major use case for this
155
+ gem is to include WYSIWYG-generated HTML parts into server-generated PDF documents.
156
+ email:
157
+ - zumkehr@puzzle.ch
158
+ executables: []
159
+ extensions: []
160
+ extra_rdoc_files: []
161
+ files:
162
+ - ".gitignore"
163
+ - ".rspec"
164
+ - ".rubocop.yml"
165
+ - ".travis.yml"
166
+ - CODE_OF_CONDUCT.md
167
+ - Gemfile
168
+ - Gemfile.lock
169
+ - LICENSE.txt
170
+ - README.md
171
+ - Rakefile
172
+ - bin/console
173
+ - bin/setup
174
+ - lib/prawn/markup.rb
175
+ - lib/prawn/markup/builders/list_builder.rb
176
+ - lib/prawn/markup/builders/nestable_builder.rb
177
+ - lib/prawn/markup/builders/table_builder.rb
178
+ - lib/prawn/markup/elements/cell.rb
179
+ - lib/prawn/markup/elements/item.rb
180
+ - lib/prawn/markup/elements/list.rb
181
+ - lib/prawn/markup/interface.rb
182
+ - lib/prawn/markup/processor.rb
183
+ - lib/prawn/markup/processor/headings.rb
184
+ - lib/prawn/markup/processor/images.rb
185
+ - lib/prawn/markup/processor/lists.rb
186
+ - lib/prawn/markup/processor/tables.rb
187
+ - lib/prawn/markup/processor/text.rb
188
+ - lib/prawn/markup/support/hash_merger.rb
189
+ - lib/prawn/markup/support/normalizer.rb
190
+ - lib/prawn/markup/support/size_converter.rb
191
+ - lib/prawn/markup/version.rb
192
+ - prawn-markup.gemspec
193
+ homepage: https://github.com/puzzle/prawn-markup
194
+ licenses:
195
+ - MIT
196
+ metadata: {}
197
+ post_install_message:
198
+ rdoc_options: []
199
+ require_paths:
200
+ - lib
201
+ required_ruby_version: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ required_rubygems_version: !ruby/object:Gem::Requirement
207
+ requirements:
208
+ - - ">="
209
+ - !ruby/object:Gem::Version
210
+ version: '0'
211
+ requirements: []
212
+ rubyforge_project:
213
+ rubygems_version: 2.4.8
214
+ signing_key:
215
+ specification_version: 4
216
+ summary: Parse simple HTML markup to include in Prawn PDFs
217
+ test_files: []